Doldurmayı kullanarak CBC modunda simetrik şifre çözmedeki zamanlama açıkları

Microsoft, çok belirli durumlar dışında, doğrulanabilir doldurma uygulandığında, şifre metninin bütünlüğünü sağlamadan cipher-Block-Chaining (CBC) simetrik şifreleme moduyla şifrelenen verilerin şifresini çözmenin artık güvenli olmadığına inanıyor. Bu karar şu anda bilinen şifreleme araştırmalarını temel alır.

Giriş

Doldurma kahini saldırısı, saldırganın anahtarı bilmeden verilerin içeriğinin şifresini çözmesine olanak tanıyan şifrelenmiş verilere karşı yapılan bir saldırı türüdür.

Kahin, saldırgana yürüttkleri eylemin doğru olup olmadığı hakkında bilgi veren bir "göster" anlamına gelir. Bir çocukla tahta veya kart oyunu oynadığınızı düşünün. Yüzleri büyük bir gülümsemeyle aydınlandığında, iyi bir hamle yapacaklarını düşündükleri için, bu bir kahindir. Rakip olarak bir sonraki hamlenizi uygun şekilde planlamak için bu kahini kullanabilirsiniz.

Doldurma, belirli bir şifreleme terimidir. Verilerinizi şifrelemek için kullanılan algoritmalar olan bazı şifreler, her bloğun sabit boyutta olduğu veri blokları üzerinde çalışır. Şifrelemek istediğiniz veriler blokları doldurmak için doğru boyutta değilse, veriler doldurulana kadar doldurulur. Birçok doldurma biçimi, özgün giriş doğru boyutta olsa bile doldurmanın her zaman mevcut olmasını gerektirir. Bu, doldurmanın şifre çözme sırasında her zaman güvenli bir şekilde kaldırılmasını sağlar.

İki şeyi bir araya getiren, doldurma kahini içeren bir yazılım uygulaması, şifresi çözülen verilerin geçerli doldurmaya sahip olup olmadığını ortaya koyar. Kahin, "Geçersiz doldurma" yazan bir değeri döndürmek kadar basit veya geçersiz bir bloğun aksine geçerli bir bloğu işlemek için ölçülebilir farklı bir zaman almak gibi daha karmaşık bir şey olabilir.

Blok tabanlı şifrelemeler, mod olarak adlandırılan ve ilk bloktaki verilerin ikinci bloktaki verilerle ilişkisini belirleyen başka bir özelliğe sahiptir. En yaygın kullanılan modlardan biri CBC'dir. CBC, Başlatma Vektöru (IV) olarak bilinen ilk rastgele bir bloğu tanıtır ve statik şifrelemenin sonucuyla önceki bloğu birleştirerek aynı iletiyi aynı anahtarla şifrelemenin her zaman aynı şifrelenmiş çıkışı üretmemesi için bunu yapar.

Saldırgan, CBC verilerinin nasıl yapılandırıldığıyla birlikte bir doldurma kahini kullanarak kahini kullanıma sunan koda biraz değiştirilmiş iletiler gönderebilir ve kahin verilerin doğru olduğunu söyleyene kadar veri göndermeye devam edebilir. Bu yanıttan, saldırgan iletinin şifresini bayt bayt çözebilir.

Modern bilgisayar ağları o kadar yüksek kalitededir ki, saldırgan uzak sistemlerde yürütme süresindeki çok küçük (0,1 ms'den az) farkları algılayabilir. Başarılı bir şifre çözme işleminin yalnızca veriler üzerinde oynanmadığında gerçekleşebileceğini varsayan uygulamalar, başarılı ve başarısız şifre çözme farklarını gözlemlemek için tasarlanmış araçlardan saldırılara karşı savunmasız olabilir. Bu zamanlama farkı bazı dillerde veya kitaplıklarda diğerlerinden daha önemli olsa da, uygulamanın hataya yanıtı dikkate alındığında bunun tüm diller ve kitaplıklar için pratik bir tehdit olduğu düşünülmektedir.

Bu saldırı, şifrelenmiş verileri değiştirme ve sonucu kahinle test etme özelliğine dayanır. Saldırıyı tam olarak azaltmanın tek yolu, şifrelenmiş verilerde yapılan değişiklikleri algılamak ve bu veriler üzerinde herhangi bir eylem gerçekleştirmeyi reddetmektir. Bunu yapmak için standart yol, veriler için bir imza oluşturmak ve herhangi bir işlem gerçekleştirilmeden önce bu imzayı doğrulamaktır. İmza doğrulanabilir olmalıdır, saldırgan tarafından oluşturulamaz, aksi takdirde şifrelenmiş verileri değiştirir ve değiştirilen verilere göre yeni bir imza hesaplar. Ortak bir uygun imza türü, anahtarlı karma ileti kimlik doğrulama kodu (HMAC) olarak bilinir. HMAC, yalnızca HMAC'yi üreten kişi ve bunu doğrulayan kişi tarafından bilinen bir gizli anahtar alması bakımından sağlama toplamından farklıdır. Anahtara sahip olmadan doğru bir HMAC üretemezsiniz. Verilerinizi aldığınızda şifrelenmiş verileri alır, sizin ve gönderenin paylaştığı gizli anahtarı kullanarak HMAC'yi bağımsız olarak hesaplar ve ardından gönderdiği HMAC'yi hesapladığınız anahtarla karşılaştırırsınız. Bu karşılaştırmanın sabit bir süre olması gerekir, aksi takdirde başka bir algılanabilir kahin ekleyerek farklı bir saldırı türüne izin verdiniz.

Özetle, doldurulmuş CBC blok şifrelerini güvenli bir şekilde kullanmak için, verilerin şifresini çözmeye çalışmadan önce sabit bir zaman karşılaştırması kullanarak doğruladığınız bir HMAC (veya başka bir veri bütünlüğü denetimi) ile birleştirmeniz gerekir. Değiştirilen tüm iletilerin yanıt üretmesi aynı süreden sonra geldiğinden, saldırı engellenir.

Kimler savunmasızdır?

Bu güvenlik açığı, kendi şifreleme ve şifre çözme işlemlerini gerçekleştiren hem yönetilen hem de yerel uygulamalar için geçerlidir. Buna şunlar dahildir:

  • Sunucuda daha sonra şifresini çözmek için bir tanımlama bilgisini şifreleyen bir uygulama.
  • Kullanıcıların sütunları daha sonra şifresi çözülen bir tabloya veri ekleme olanağı sağlayan bir veritabanı uygulaması.
  • Aktarımdaki verileri korumak için paylaşılan anahtar kullanarak şifrelemeye dayalı bir veri aktarımı uygulaması.
  • TLS tünelinin "içindeki" iletileri şifreleyen ve şifresini çözen bir uygulama.

Bu senaryolarda YALNıZCA TLS kullanmanın sizi korumayabileceğini unutmayın.

Güvenlik açığı olan bir uygulama:

  • PKCS#7 veya ANSI X.923 gibi doğrulanabilir bir doldurma moduyla CBC şifreleme modunu kullanarak verilerin şifresini çözer.
  • Veri bütünlüğü denetimi gerçekleştirmeden şifre çözme işlemini gerçekleştirir (MAC veya asimetrik dijital imza aracılığıyla).

Bu, Şifreleme İletisi Söz Dizimi (PKCS#7/CMS) EnvelopedData yapısı gibi bu temel öğeler üzerinde soyutlamaların üzerine kurulu uygulamalar için de geçerlidir.

Araştırma, microsoft'un iletinin iyi bilinen veya tahmin edilebilir bir alt bilgi yapısına sahip olduğunda ISO 10126 eşdeğeri doldurma ile doldurulmuş CBC iletileri hakkında daha fazla endişelenmesine neden olmuştur. Örneğin, W3C XML Şifreleme Söz Dizimi ve İşleme Önerisi (xmlenc, EncryptedXml) kuralları altında hazırlanan içerik. İletiyi imzalamak ve şifrelemek için W3C kılavuzu o sırada uygun olarak kabul edilmiş olsa da, Microsoft artık her zaman şifrele-sonra-imzayı gerçekleştirmeyi önerir.

Asimetrik anahtar ile rastgele bir ileti arasında doğal bir güven ilişkisi olmadığından uygulama geliştiricileri her zaman asimetrik imza anahtarının uygulanabilirliğini doğrulama konusunda dikkatli olmalıdır.

Ayrıntılar

Geçmişte, HMAC veya RSA imzaları gibi araçları kullanarak önemli verileri şifrelemenin ve kimlik doğrulamasının önemli olduğu konusunda fikir birliği sağlanmıştır. Ancak, şifreleme ve kimlik doğrulama işlemlerinin nasıl sıralandığını gösteren daha az açık bir kılavuz vardır. Bu makalede açıklanan güvenlik açığı nedeniyle, Microsoft'un kılavuzu artık her zaman "şifrele-sonra-imza" paradigması kullanmaktır. Yani, önce simetrik anahtar kullanarak verileri şifreleyin, ardından şifre metni (şifrelenmiş veriler) üzerinde bir MAC veya asimetrik imza hesaplayın. Verilerin şifresini çözerken ters işlemi gerçekleştirin. İlk olarak, şifreleme metninin MAC'ini veya imzasını onaylayın, ardından şifresini çöz.

"Doldurma kahini saldırıları" olarak bilinen bir güvenlik açığı sınıfının 10 yıldan uzun süredir mevcut olduğu bilinmektedir. Bu güvenlik açıkları, bir saldırganın AES ve 3DES gibi simetrik blok algoritmalarıyla şifrelenen verilerin şifresini çözmek için veri bloğu başına en fazla 4096 deneme kullanmasına olanak sağlar. Bu güvenlik açıkları, blok şifrelemelerinin en sık sonunda doğrulanabilir doldurma verileriyle kullanıldığı gerçeğinden yararlanır. Bir saldırganın şifre metniyle oynaması ve kurcalama işleminin sonunda doldurma biçiminde bir hataya neden olup olmadığını öğrenmesi durumunda, saldırganın verilerin şifresini çözebileceği bulunmuştur.

Başlangıçta pratik saldırılar, doldurmanın geçerli olup olmadığına bağlı olarak farklı hata kodları döndüren hizmetlere (örneğin, ASP.NET güvenlik açığı MS10-070) dayanıyordu. Ancak Microsoft artık benzer saldırıların yalnızca geçerli ve geçersiz doldurma işlemleri arasındaki zamanlama farklarını kullanarak gerçekleştirilmesinin pratik olduğuna inanıyor.

Şifreleme şemasında bir imzanın olması ve belirli bir veri uzunluğu (içeriklerden bağımsız olarak) için sabit bir çalışma zamanı ile imza doğrulamasının gerçekleştirilmesi koşuluyla, veri bütünlüğü bir saldırgana yan kanal üzerinden herhangi bir bilgi yaymadan doğrulanabilir. Bütünlük denetimi üzerinde oynanmış iletileri reddettiği için, doldurma kahini tehdidi azaltılır.

Rehber

Microsoft her şeyden önce, gizliliği olan tüm verilerin Güvenli Yuva Katmanı'na (SSL) ardıl olan Aktarım Katmanı Güvenliği (TLS) üzerinden iletilmesini önerir.

Ardından, uygulamanızı şu şekilde analiz edin:

  • Hangi şifrelemeyi gerçekleştirdiğiniz ve kullandığınız platformlar ve API'ler tarafından hangi şifrelemenin sağlandığını tam olarak anlayın.
  • CBC modunda AES ve 3DES gibi bir simetrik blok şifreleme algoritmasının her katmanındaki her kullanımın gizli anahtarlı veri bütünlüğü denetimi (asimetrik imza, HMAC veya şifreleme modunu GCM veya CCM gibi kimliği doğrulanmış bir şifreleme (AE) moduna değiştirmek için) kullanımını içerdiğinden emin olun.

Geçerli araştırmalara dayanarak, kimlik doğrulaması ve şifreleme adımları AE olmayan şifreleme modları için bağımsız olarak gerçekleştirildiğinde, şifreleme metninin (şifrele-sonra-işaret) kimliğini doğrulamanın en iyi genel seçenek olduğuna inanılmaktadır. Ancak şifrelemeye tek bir boyuta uygun doğru yanıt yoktur ve bu genelleştirme, profesyonel bir kriptograftan gelen yönlendirilmiş öneriler kadar iyi değildir.

Mesajlaşma biçimlerini değiştiremeyen ancak kimliği doğrulanmamış CBC şifre çözme işlemi gerçekleştiren uygulamaların aşağıdaki gibi risk azaltmaları dahil etmeye çalışması teşvik edilir:

  • Şifre çözücüye doldurmayı doğrulama veya kaldırma izni vermeden şifre çözme:
    • Uygulanan tüm doldurmaların yine de kaldırılması veya yoksayılması gerekir; yükü uygulamanıza taşırsınız.
    • Bunun avantajı, doldurma doğrulama ve kaldırma işleminin diğer uygulama verileri doğrulama mantığına dahil edilebiliyor olmasıdır. Doldurma doğrulaması ve veri doğrulama işlemi sürekli olarak yapılabiliyorsa tehdit azalır.
    • Doldurmanın yorumlanması algılanan ileti uzunluğunu değiştirdiğinden, bu yaklaşımdan yayılan zamanlama bilgileri yine de olabilir.
  • Şifre çözme doldurma modunu ISO10126 olarak değiştirin:
    • ISO10126 şifre çözme doldurması hem PKCS7 şifreleme doldurması hem de ANSIX923 şifreleme doldurmasıyla uyumludur.
    • Modun değiştirilmesi, doldurma kahini bilgisini bloğun tamamı yerine 1 bayt'a düşürür. Ancak, içerikte kapanış XML öğesi gibi iyi bilinen bir alt bilgi varsa, ilgili saldırılar iletinin geri kalanına saldırmaya devam edebilir.
    • Bu, saldırganın aynı düz metni farklı bir ileti uzaklığıyla birden çok kez şifrelenecek şekilde zorladığı durumlarda düz metin kurtarmayı da engellemez.
  • Zamanlama sinyalini nemlendirmek için bir şifre çözme çağrısının değerlendirmesini geçitle:
    • Bekletme süresi hesaplaması, doldurma içeren herhangi bir veri kesimi için şifre çözme işleminin süresi üst sınırını aşan bir minimum değere sahip olmalıdır.
    • Zaman hesaplamaları, yüksek çözünürlüklü zaman damgaları alma başlığı altında verilen kılavuza göre yapılmalıdır; bunun için iki sistem zaman damgasını (NTP ayarlama hatalarına tabi) kullanma Environment.TickCount (yuvarlama/taşmaya tabi) kullanma veya çıkarma işlemleri yapılmamalıdır.
    • Zaman hesaplamaları, yönetilen veya C++ uygulamalarındaki tüm olası özel durumlar dahil olmak üzere şifre çözme işlemine dahil edilmelidir; yalnızca sonuna doldurulmamalıdır.
    • Başarı veya başarısızlık henüz belirlendiyse, zamanlama geçidinin süresi dolduğunda hata döndürmesi gerekir.
  • Kimliği doğrulanmamış şifre çözme işlemi gerçekleştiren hizmetlerin " geçersiz" iletilerin akın ettiğini algılamak için izleme gerçekleştirilmelidir.
    • Bu sinyalin hem hatalı pozitifler (yasal olarak bozuk veriler) hem de hatalı negatifler (algılamayı önlemek için saldırıyı yeterince uzun bir süreye yayma) taşıdığını unutmayın.

Güvenlik açığı bulunan kodu bulma - yerel uygulamalar

Windows Şifrelemesi: Yeni Nesil (CNG) kitaplığına göre oluşturulan programlar için:

  • Şifre çözme çağrısı BCryptDecrypt'e yapılır ve bayrağı belirtilirBCRYPT_BLOCK_PADDING.
  • Anahtar tanıtıcısı, BCRYPT_CHAINING_MODE olarak ayarlanmış BCryptSetProperty çağrılarak BCRYPT_CHAIN_MODE_CBCbaşlatıldı.
    • Varsayılan olduğundan BCRYPT_CHAIN_MODE_CBC , etkilenen kod için BCRYPT_CHAINING_MODEherhangi bir değer atamamış olabilir.

Eski Windows Şifreleme API'sine göre oluşturulan programlar için:

  • Şifre çözme çağrısı ile Final=TRUECryptDecrypt'e yapılır.
  • Anahtar tanıtıcısı, KP_MODE olarak ayarlanmış CryptSetKeyParam çağrılarak CRYPT_MODE_CBCbaşlatıldı.
    • Varsayılan olduğundan CRYPT_MODE_CBC , etkilenen kod için KP_MODEherhangi bir değer atamamış olabilir.

Güvenlik açığı bulunan kodu bulma - yönetilen uygulamalar

Güvenlik açığı bulunan kodu bulma - şifreleme iletisi söz dizimi

Şifrelenmiş içeriği AES'nin CBC modunu kullanan kimliği doğrulanmamış CMS EnvelopedData iletisi (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) veya RC2 (1.2.840.113549.3.2) savunmasız, ve CBC modunda diğer blok şifreleme algoritmalarını kullanan iletiler.

Akış şifreleri bu güvenlik açığına açık olmasa da Microsoft, ContentEncryptionAlgorithm değerini inceleyerek her zaman verilerin kimliğini doğrulamanızı önerir.

Yönetilen uygulamalar için, cms envelopedData blobu'na System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[])geçirilen herhangi bir değer olarak algılanabilir.

Yerel uygulamalar için bir CMS EnvelopedData blobu, CryptMsg CMSG_TYPE_PARAMCMSG_ENVELOPED Update aracılığıyla cms tanıtıcısına sağlanan herhangi bir değer olarak algılanabilir ve/veya cms tanıtıcısı daha sonra CryptMsgControl aracılığıyla bir CMSG_CTRL_DECRYPT yönerge gönderilir.

Güvenlik açığı bulunan kod örneği - yönetilen

Bu yöntem bir tanımlama bilgisini okur ve şifresini çözer ve hiçbir veri bütünlüğü denetimi görünmez. Bu nedenle, bu yöntem tarafından okunan bir tanımlama bilgisinin içeriği, bunu alan kullanıcı veya şifrelenmiş tanımlama bilgisi değerini alan herhangi bir saldırgan tarafından saldırıya uğrayabilir.

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

Aşağıdaki örnek kod,

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

cipher_algorithm_id burada ve hmac_algorithm_id algoritma tanımlayıcıları, bu algoritmaların uygulama yerel (standart olmayan) gösterimleridir. Bu tanımlayıcılar, boş birleştirilmiş bayt akışı yerine mevcut mesajlaşma protokolünüzün diğer bölümlerinde anlamlı olabilir.

Bu örnek ayrıca hem şifreleme anahtarı hem de HMAC anahtarı türetmek için tek bir ana anahtar kullanır. Bu, hem tek anahtarlı bir uygulamayı çift anahtarlı bir uygulamaya dönüştürmek hem de iki anahtarın farklı değerler olarak tutulmasını teşvik etmek için kolaylık sağlar. Ayrıca HMAC anahtarının ve şifreleme anahtarının eşitlemeden çıkamazsınız.

Bu örnek, şifreleme veya şifre çözme için bir Stream kabul etmez. Geçerli veri biçimi, değer şifre metninden önce geldiği için hmac_tag tek geçişli şifrelemeyi zorlaştırır. Ancak, ayrıştırıcıyı daha basit tutmak için tüm sabit boyutlu öğeleri başlangıçta tuttuğundan bu biçim seçildi. Bu veri biçimiyle tek geçişli şifre çözme mümkündür, ancak bir uygulayıcı, TransformFinalBlock çağrısından önce GetHashAndReset çağrısı yapmak ve sonucu doğrulamak için uyarılır. Akış şifrelemesi önemliyse farklı bir AE modu gerekebilir.

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

Ayrıca bkz.