Bagikan melalui


Kerentanan waktu dengan dekripsi simetris mode CBC dengan menggunakan padding

Microsoft percaya bahwa tidak lagi aman untuk mendekripsi data yang dienkripsi dengan mode Cipher-Block-Chaining (CBC) enkripsi simetris ketika padding yang dapat diverifikasi telah diterapkan tanpa terlebih dahulu memastikan integritas ciphertext, kecuali untuk keadaan yang sangat spesifik. Penilaian ini didasarkan pada penelitian kriptografi yang saat ini diketahui.

Pengantar

Serangan oracle padding adalah jenis serangan terhadap data terenkripsi yang memungkinkan penyerang untuk mendekripsi konten data, tanpa mengetahui kuncinya.

Oracle mengacu pada "tell" yang memberikan informasi kepada penyerang tentang apakah tindakan yang mereka jalankan sudah benar atau tidak. Bayangkan bermain papan atau permainan kartu dengan anak-anak. Ketika wajah mereka menyala dengan senyum lebar karena mereka pikir mereka akan membuat gerakan yang baik, itu adalah oracle. Anda, sebagai lawan, dapat menggunakan oracle ini untuk merencanakan langkah Anda berikutnya dengan tepat.

Padding adalah istilah kriptografi tertentu. Beberapa cipher, yang merupakan algoritma yang digunakan untuk mengenkripsi data Anda, bekerja pada blok data di mana setiap blok berukuran tetap. Jika data yang ingin Anda enkripsi bukan ukuran yang tepat untuk mengisi blok, data Anda akan di-padding hingga data terisi. Banyak bentuk padding mengharuskan padding selalu ada, bahkan jika input asli berukuran tepat. Ini memungkinkan padding untuk selalu dihapus dengan aman setelah dekripsi.

Menyatukan dua hal tersebut, implementasi perangkat lunak dengan oracle padding mengungkapkan apakah data yang didekripsi memiliki padding yang valid. Oracle bisa menjadi sesuatu yang sesederhana mengembalikan nilai yang mengatakan "padding tidak valid" atau sesuatu yang lebih rumit seperti mengambil waktu yang terukur berbeda untuk memproses blok yang valid dibandingkan dengan blok yang tidak valid.

Cipher berbasis blok memiliki properti lain, yang disebut mode, yang menentukan hubungan data di blok pertama ke data di blok kedua, dan sebagainya. Salah satu mode yang paling umum digunakan adalah CBC. CBC memperkenalkan blok acak awal, yang dikenal sebagai Vektor Inisialisasi (IV), dan menggabungkan blok sebelumnya dengan hasil enkripsi statis untuk membuatnya sedih sehingga mengenkripsi pesan yang sama dengan kunci yang sama tidak selalu menghasilkan output terenkripsi yang sama.

Penyerang dapat menggunakan oracle padding, dalam kombinasi dengan bagaimana data CBC disusun, untuk mengirim pesan yang sedikit diubah ke kode yang mengekspos oracle, dan terus mengirim data sampai oracle memberi tahu mereka bahwa data sudah benar. Dari respons ini, penyerang dapat mendekripsi pesan byte demi byte.

Jaringan komputer modern berkualitas tinggi sehingga penyerang dapat mendeteksi perbedaan yang sangat kecil (kurang dari 0,1 ms) dalam waktu eksekusi pada sistem jarak jauh. Aplikasi yang mengasumsikan bahwa dekripsi yang berhasil hanya dapat terjadi ketika data tidak diubah mungkin rentan terhadap serangan dari alat yang dirancang untuk mengamati perbedaan dekripsi yang berhasil dan tidak berhasil. Meskipun perbedaan waktu ini mungkin lebih signifikan dalam beberapa bahasa atau pustaka daripada yang lain, sekarang diyakini bahwa ini adalah ancaman praktis untuk semua bahasa dan pustaka ketika respons aplikasi terhadap kegagalan dipertanggungjawabkan.

Serangan ini bergantung pada kemampuan untuk mengubah data terenkripsi dan menguji hasilnya dengan oracle. Satu-satunya cara untuk sepenuhnya mengurangi serangan adalah dengan mendeteksi perubahan pada data terenkripsi dan menolak untuk melakukan tindakan apa pun di atasnya. Cara standar untuk melakukan ini adalah dengan membuat tanda tangan untuk data dan memvalidasi tanda tangan tersebut sebelum operasi apa pun dilakukan. Tanda tangan harus dapat diverifikasi, tanda tangan tidak dapat dibuat oleh penyerang, jika tidak, mereka akan mengubah data terenkripsi, lalu menghitung tanda tangan baru berdasarkan data yang diubah. Salah satu jenis umum tanda tangan yang sesuai dikenal sebagai kode autentikasi pesan keyed-hash (HMAC). HMAC berbeda dari checksum karena dibutuhkan kunci rahasia, yang hanya diketahui oleh orang yang memproduksi HMAC dan kepada orang yang memvalidasinya. Tanpa memiliki kunci, Anda tidak dapat menghasilkan HMAC yang benar. Saat Anda menerima data, Anda akan mengambil data terenkripsi, menghitung HMAC secara independen menggunakan kunci rahasia yang Anda dan pengirim bagikan, lalu membandingkan HMAC yang mereka kirim dengan yang Anda hitung. Perbandingan ini harus waktu konstan, jika tidak, Anda telah menambahkan oracle lain yang dapat dideteksi, memungkinkan jenis serangan yang berbeda.

Singkatnya, untuk menggunakan cipher blok CBC berlapis dengan aman, Anda harus menggabungkannya dengan HMAC (atau pemeriksaan integritas data lain) yang Anda validasi menggunakan perbandingan waktu konstan sebelum mencoba mendekripsi data. Karena semua pesan yang diubah membutuhkan waktu yang sama untuk menghasilkan respons, serangan dicegah.

Pihak yang rentan

Kerentanan ini berlaku untuk aplikasi terkelola dan asli yang melakukan enkripsi dan dekripsi mereka sendiri. Ini termasuk, misalnya:

  • Aplikasi yang mengenkripsi cookie untuk dekripsi nanti di server.
  • Aplikasi database yang menyediakan kemampuan bagi pengguna untuk menyisipkan data ke dalam tabel yang kolomnya kemudian didekripsi.
  • Aplikasi transfer data yang bergantung pada enkripsi menggunakan kunci bersama untuk melindungi data saat transit.
  • Aplikasi yang mengenkripsi dan mendekripsi pesan "di dalam" terowongan TLS.

Perhatikan bahwa menggunakan TLS saja mungkin tidak melindungi Anda dalam skenario ini.

Aplikasi yang rentan:

  • Mendekripsi data menggunakan mode cipher CBC dengan mode padding yang dapat diverifikasi, seperti PKCS#7 atau ANSI X.923.
  • Melakukan dekripsi tanpa melakukan pemeriksaan integritas data (melalui MAC atau tanda tangan digital asimetris).

Ini juga berlaku untuk aplikasi yang dibangun di atas abstraksi di atas primitif ini, seperti struktur EnvelopedData Sintaks Pesan Kriptografi (PKCS#7/CMS).

Penelitian telah membuat Microsoft lebih khawatir tentang pesan CBC yang dilengkapi dengan padding setara ISO 10126 ketika pesan memiliki struktur footer yang terkenal atau dapat diprediksi. Misalnya, konten yang disiapkan di bawah aturan Sintaks Enkripsi XML W3C dan Rekomendasi Pemrosesan (xmlenc, EncryptedXml). Meskipun panduan W3C untuk menandatangani pesan kemudian mengenkripsi dianggap sesuai pada saat itu, Microsoft sekarang merekomendasikan untuk selalu melakukan tanda tangan enkripsi kemudian.

Pengembang aplikasi harus selalu berhati-hati untuk memverifikasi penerapan kunci tanda tangan asimetris, karena tidak ada hubungan kepercayaan yang melekat antara kunci asimetris dan pesan arbitrer.

Detail

Secara historis, ada konsekuensi bahwa penting untuk mengenkripsi dan mengautentikasi data penting, menggunakan cara-cara seperti tanda tangan HMAC atau RSA. Namun, ada panduan yang kurang jelas tentang cara urutan operasi enkripsi dan autentikasi. Karena kerentanan yang dijabarkan dalam artikel ini, panduan Microsoft sekarang adalah untuk selalu menggunakan paradigma "encrypt-then-sign". Artinya, pertama-tama enkripsi data menggunakan kunci konten, lalu komputasi MAC atau tanda tangan asimetris melalui ciphertext (data terenkripsi). Saat mendekripsi data, lakukan kebalikannya. Pertama, konfirmasikan MAC atau tanda tangan ciphertext, lalu dekripsi.

Kelas kerentanan yang dikenal sebagai "serangan oracle padding" telah diketahui ada selama lebih dari 10 tahun. Kerentanan ini memungkinkan penyerang untuk mendekripsi data yang dienkripsi oleh algoritma blok simetris, seperti AES dan 3DES, menggunakan tidak lebih dari 4096 upaya per blok data. Kerentanan ini memanfaatkan fakta bahwa cipher blok paling sering digunakan dengan data padding yang dapat diverifikasi di akhir. Ditemukan bahwa jika penyerang dapat mengubah ciphertext dan mencari tahu apakah pengubahan menyebabkan kesalahan dalam format padding di akhir, penyerang dapat mendekripsi data.

Awalnya, serangan praktis didasarkan pada layanan yang akan mengembalikan kode kesalahan yang berbeda berdasarkan apakah padding valid, seperti ASP.NET kerentanan MS10-070. Namun, Microsoft sekarang percaya bahwa praktis untuk melakukan serangan serupa hanya menggunakan perbedaan waktu antara pemrosesan padding yang valid dan tidak valid.

Asalkan skema enkripsi menggunakan tanda tangan dan verifikasi tanda tangan dilakukan dengan runtime tetap untuk panjang data tertentu (terlepas dari konten), integritas data dapat diverifikasi tanpa memancarkan informasi apa pun kepada penyerang melalui saluran samping. Karena pemeriksaan integritas menolak pesan yang dirusak, ancaman oracle padding dimitigasi.

Panduan

Pertama dan terpenting, Microsoft merekomendasikan bahwa setiap data yang memiliki kerahasiaan perlu dikirimkan melalui Transport Layer Security (TLS), penerus Secure Sockets Layer (SSL).

Selanjutnya, analisis aplikasi Anda untuk:

  • Pahami dengan tepat enkripsi apa yang Anda lakukan dan enkripsi apa yang disediakan oleh platform dan API yang Anda gunakan.
  • Pastikan bahwa setiap penggunaan di setiap lapisan algoritma cipher blok simetris, seperti AES dan 3DES, dalam mode CBC menggabungkan penggunaan pemeriksaan integritas data kunci rahasia (tanda tangan asimetris, HMAC, atau untuk mengubah mode cipher ke mode enkripsi terautentikasi (AE) seperti GCM atau CCM).

Berdasarkan penelitian saat ini, umumnya diyakini bahwa ketika langkah autentikasi dan enkripsi dilakukan secara independen untuk mode enkripsi non-AE, autentikasi ciphertext (encrypt-then-sign) adalah pilihan umum terbaik. Namun, tidak ada jawaban yang benar untuk semua ukuran kriptografi dan generalisasi ini tidak sebagus saran yang diarahkan dari kriptografer profesional.

Aplikasi yang tidak dapat mengubah format olahpesan mereka tetapi melakukan dekripsi CBC yang tidak diautentikasi didorong untuk mencoba menggabungkan mitigasi seperti:

  • Dekripsi tanpa mengizinkan dekripsi untuk memverifikasi atau menghapus padding:
    • Setiap padding yang diterapkan masih perlu dihapus atau diabaikan, Anda memindahkan beban ke dalam aplikasi Anda.
    • Manfaatnya adalah verifikasi dan penghapusan padding dapat dimasukkan ke dalam logika verifikasi data aplikasi lainnya. Jika verifikasi padding dan verifikasi data dapat dilakukan dalam waktu konstan, ancaman berkurang.
    • Karena interpretasi padding mengubah panjang pesan yang dirasakan, mungkin masih ada informasi waktu yang dikeluarkan dari pendekatan ini.
  • Ubah mode padding dekripsi menjadi ISO10126:
    • Padding dekripsi ISO10126 kompatibel dengan padding enkripsi PKCS7 dan padding enkripsi ANSIX923.
    • Mengubah mode akan mengurangi pengetahuan oracle padding menjadi 1 byte, bukan seluruh blok. Namun, jika konten memiliki footer terkenal, seperti elemen XML penutup, serangan terkait dapat terus menyerang sisa pesan.
    • Ini juga tidak mencegah pemulihan teks biasa dalam situasi di mana penyerang dapat memaksa teks biasa yang sama untuk dienkripsi beberapa kali dengan offset pesan yang berbeda.
  • Gerbang evaluasi panggilan dekripsi untuk meredam sinyal waktu:
    • Komputasi waktu penahanan harus memiliki minimal melebihi jumlah waktu maksimum yang akan diambil operasi dekripsi untuk segmen data apa pun yang berisi padding.
    • Komputasi waktu harus dilakukan sesuai dengan panduan dalam Memperoleh stempel waktu resolusi tinggi, bukan dengan menggunakan Environment.TickCount (tunduk pada roll-over/overflow) atau mengurangi dua tanda waktu sistem (tunduk pada kesalahan penyesuaian NTP).
    • Perhitungan waktu harus mencakup operasi dekripsi termasuk semua pengecualian potensial dalam aplikasi terkelola atau C++, tidak hanya diisi sampai akhir.
    • Jika keberhasilan atau kegagalan telah ditentukan, gerbang waktu harus mengembalikan kegagalan saat habis masa berlakunya.
  • Layanan yang melakukan dekripsi yang tidak diautentikasi harus memiliki pemantauan untuk mendeteksi bahwa banjir pesan "tidak valid" telah masuk.
    • Ingatlah bahwa sinyal ini membawa positif palsu (data yang benar-benar rusak) dan negatif palsu (menyebarkan serangan dalam waktu yang cukup lama untuk menghindari deteksi).

Menemukan kode yang rentan - aplikasi asli

Untuk program yang dibangun terhadap pustaka Windows Cryptography: Next Generation (CNG):

  • Panggilan dekripsi adalah ke BCryptDecrypt, menentukan bendera BCRYPT_BLOCK_PADDING.
  • Handel kunci telah diinisialisasi dengan memanggil BCryptSetProperty dengan BCRYPT_CHAINING_MODE diatur ke BCRYPT_CHAIN_MODE_CBC.
    • Karena BCRYPT_CHAIN_MODE_CBC adalah default, kode yang terpengaruh mungkin belum menetapkan nilai apa pun untuk BCRYPT_CHAINING_MODE.

Untuk program yang dibangun terhadap API Kriptografi Windows yang lebih lama:

  • Panggilan dekripsi adalah ke CryptDecrypt dengan Final=TRUE.
  • Handel kunci telah diinisialisasi dengan memanggil CryptSetKeyParam dengan KP_MODE diatur ke CRYPT_MODE_CBC.
    • Karena CRYPT_MODE_CBC adalah default, kode yang terpengaruh mungkin belum menetapkan nilai apa pun untuk KP_MODE.

Menemukan kode yang rentan - aplikasi terkelola

Menemukan kode yang rentan - sintaks pesan kriptografi

Pesan EnvelopedData CMS yang tidak diautentikasi yang konten terenkripsinya menggunakan mode CBC 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) atau RC2 (1.2.840.113549.3.2) rentan, serta pesan yang menggunakan algoritma cipher blok lainnya dalam mode CBC.

Meskipun cipher aliran tidak rentan terhadap kerentanan khusus ini, Microsoft merekomendasikan untuk selalu mengautentikasi data selama memeriksa nilai ContentEncryptionAlgorithm.

Untuk aplikasi terkelola, blob CMS EnvelopedData dapat dideteksi sebagai nilai apa pun yang diteruskan ke System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]).

Untuk aplikasi asli, blob CMS EnvelopedData dapat dideteksi sebagai nilai apa pun yang diberikan ke pegangan CMS melalui CryptMsgUpdate yang menghasilkan CMSG_TYPE_PARAMCMSG_ENVELOPED dan/atau pegangan CMS kemudian mengirimkan instruksi CMSG_CTRL_DECRYPT melalui CryptMsgControl.

Contoh kode yang rentan - terkelola

Metode ini membaca cookie dan mendekripsinya dan tidak ada pemeriksaan integritas data yang terlihat. Oleh karena itu, konten cookie yang dibaca oleh metode ini dapat diserang oleh pengguna yang menerimanya, atau oleh penyerang mana pun yang telah mendapatkan nilai cookie terenkripsi.

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();
    }
}

Kode sampel berikut menggunakan format pesan nonstandar dari

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

di mana pengidentifikasi algoritma cipher_algorithm_id dan hmac_algorithm_id adalah representasi lokal aplikasi (nonstandar) dari algoritma tersebut. Pengidentifikasi ini mungkin masuk akal di bagian lain dari protokol olahpesan Anda yang ada dan bukan sebagai bytestream gabungan yang kosong.

Contoh ini juga menggunakan satu kunci master untuk memperoleh kunci enkripsi dan kunci HMAC. Ini disediakan baik sebagai kemudahan untuk mengubah aplikasi dengan satu kunci menjadi aplikasi dengan dua kunci, dan untuk mendorong menjaga kedua kunci sebagai nilai yang berbeda. Ini lebih lanjut menjamin bahwa kunci HMAC dan kunci enkripsi tidak dapat keluar dari sinkronisasi.

Sampel ini tidak menerima Stream untuk enkripsi atau dekripsi. Format data saat ini menyulitkan enkripsi satu kali karena hmac_tag nilainya mendahului ciphertext. Namun, format ini dipilih karena menjaga semua elemen ukuran tetap di awal untuk menjaga pengurai lebih sederhana. Dengan format data ini, dekripsi satu arah dimungkinkan, meskipun pelaksana diperingatkan untuk memanggil GetHashAndReset dan memverifikasi hasilnya sebelum memanggil TransformFinalBlock. Jika enkripsi streaming penting, mode AE yang berbeda mungkin diperlukan.

// ==++==
//
//   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;
        }
    }
}

Lihat juga