Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
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.
Pendahuluan
Serangan oracle padding adalah jenis serangan terhadap data terenkripsi yang memungkinkan penyerang untuk mendekripsi konten data, tanpa mengetahui kuncinya.
Oracle mengacu pada "sinyal" yang memberikan informasi kepada penyerang tentang apakah tindakan yang mereka lakukan benar atau tidak. Bayangkan bermain papan atau permainan kartu dengan seorang anak. Ketika wajah mereka bersinar dengan senyum lebar karena mereka berpikir mereka sedang mengambil keputusan yang baik, itu adalah pertanda baik. Anda, sebagai lawan, dapat menggunakan oracle ini untuk merencanakan langkah Anda berikutnya dengan tepat.
Padding adalah istilah kriptografi tertentu. Beberapa sandi, yang merupakan algoritma yang digunakan untuk mengenkripsi data Anda, bekerja pada blok data di mana setiap blok berukuran tetap. Jika data yang ingin dienkripsi tidak berukuran tepat untuk mengisi blok, data Anda akan diisi hingga mencukupi. Banyak bentuk padding mengharuskan agar padding selalu ada, bahkan jika ukuran input asli sudah tepat. Ini memungkinkan padding untuk selalu dihapus dengan aman setelah dekripsi.
Dengan menggabungkan kedua 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 sangat 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 agar mengenkripsi pesan yang sama dengan kunci yang sama tidak selalu menghasilkan output terenkripsi yang sama.
Penyerang dapat menggunakan oracle padding, bersama dengan cara penyusunan data CBC, untuk mengirim pesan yang sedikit diubah ke kode yang mengekspos oracle tersebut, dan terus mengirim data sampai oracle memberi tahu mereka bahwa data tersebut sudah benar. Dari respons ini, penyerang dapat mendekripsi pesan secara byte per 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 dirusak 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 diperhitungkan.
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 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 tepat dikenal sebagai kode autentikasi pesan ber-hash kunci (HMAC). HMAC berbeda dari checksum karena membutuhkan kunci rahasia, yang hanya diketahui oleh orang yang memproduksi HMAC dan orang yang memvalidasinya. Tanpa memiliki kunci, Anda tidak dapat menghasilkan HMAC yang benar. Saat 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 dilakukan dalam waktu tetap, jika tidak, Anda telah menyertakan "oracle" lain yang dapat dideteksi, sehingga memungkinkan serangan jenis yang berbeda.
Singkatnya, untuk menggunakan cipher blok CBC yang dipadding dengan aman, Anda harus menggabungkannya dengan HMAC (atau verifikasi integritas data lain) yang Anda validasi dengan cara yang memakan waktu yang konstan sebelum mencoba mendekripsi data. Karena semua pesan yang diubah membutuhkan waktu yang sama untuk menghasilkan respons, serangan dicegah.
Siapa 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).
Area terkait yang menjadi perhatian
Penelitian telah membuat Microsoft semakin khawatir tentang pesan CBC yang menggunakan padding yang setara dengan ISO 10126 ketika pesan tersebut memiliki struktur footer yang dikenal atau dapat diprediksi. Misalnya, konten yang disiapkan berdasarkan aturan dari Rekomendasi Sintaks dan Pemrosesan Enkripsi XML W3C (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 memperhatikan verifikasi penerapan kunci tanda tangan asimetris, karena tidak ada hubungan kepercayaan yang melekat antara kunci asimetris dan pesan arbitrer.
Detail lebih lanjut
Secara historis, ada konsekuensi bahwa penting untuk mengenkripsi dan mengautentikasi data penting, menggunakan cara seperti tanda tangan HMAC atau RSA. Namun, telah ada panduan yang kurang jelas tentang cara urutan operasi enkripsi dan autentikasi. Karena kerentanan yang dirinci dalam artikel ini, panduan Microsoft sekarang adalah selalu menggunakan paradigma "encrypt-then-sign". Artinya, pertama-tama enkripsi data menggunakan kunci simetris, lalu hitung MAC atau tanda tangan digital asimetris pada ciphertext (data terenkripsi). Saat mendekripsi data, lakukan pembalikan. Pertama, konfirmasi MAC atau tanda tangan dari ciphertext, setelah itu dekripsi.
Sekelompok kerentanan yang dikenal sebagai "padding oracle attack" telah ada selama lebih dari 10 tahun. Kerentanan ini memungkinkan penyerang untuk mendekripsi data yang dienkripsi oleh algoritma blok simetris, seperti AES dan 3DES, dengan tidak lebih dari 4096 percobaan per blok data. Kerentanan ini memanfaatkan fakta bahwa blok cipher paling sering digunakan dengan data padding yang dapat diverifikasi pada bagian akhir. Telah ditemukan bahwa jika penyerang dapat mengubah ciphertext dan mengetahui jika perubahan tersebut menyebabkan kesalahan dalam format padding pada bagian akhir, penyerang dapat mendekripsi data.
Awalnya, serangan praktis didasarkan pada layanan yang akan mengembalikan kode kesalahan yang berbeda berdasarkan apakah padding tersebut valid, seperti kerentanan ASP.NET MS10-070. Namun, Microsoft kini percaya bahwa lebih praktis melakukan serangan serupa dengan hanya memanfaatkan selisih waktu antara pemrosesan padding yang valid dan yang tidak valid.
Asalkan skema enkripsi menggunakan tanda tangan dan bahwa 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 telah dimodifikasi, ancaman "padding oracle" dapat diatasi.
Bimbingan
Pertama dan yang terdepan, Microsoft merekomendasikan bahwa data apa pun yang memiliki kerahasiaan perlu ditransmisikan melalui Keamanan Lapisan Transportasi (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 sandi blok simetris, seperti AES dan 3DES, dalam mode CBC menggabungkan penggunaan pemeriksaan integritas data yang dikunci 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-langkah autentikasi dan enkripsi dilakukan secara independen untuk mode enkripsi non-AE, mengautentikasi ciphertext (encrypt-then-sign) adalah opsi umum terbaik. Namun, tidak ada jawaban yang tepat yang berlaku untuk semua hal dalam kriptografi, dan generalisasi ini tidak sebaik saran dari seorang 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 dekriptor untuk memverifikasi atau menghapus pelapis.
- Setiap padding yang diterapkan masih perlu dihapus atau diabaikan, beban tersebut dipindahkan ke aplikasi Anda.
- Manfaatnya adalah verifikasi dan penghapusan padding dapat dimasukkan ke dalam logika verifikasi data aplikasi lainnya. Jika verifikasi padding dan verifikasi data dilakukan dalam waktu konstan, ancaman dapat berkurang.
- Karena interpretasi padding mengubah panjang pesan yang dirasakan, mungkin masih ada informasi waktu yang dikeluarkan dari pendekatan ini.
- Ubah mode padding untuk dekripsi menjadi ISO10126.
- Padding dekripsi ISO10126 kompatibel dengan padding enkripsi PKCS7 dan padding enkripsi ANSIX923.
- Mengubah mode mengurangi pengetahuan oracle padding menjadi 1 byte alih-alih 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:
- Perhitungan waktu penahanan harus memiliki waktu minimum yang melebihi waktu maksimum yang diperlukan oleh operasi dekripsi untuk setiap segmen data 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).
- Komputasi waktu harus termasuk operasi dekripsi serta semua pengecualian potensial dalam aplikasi terkelola atau C++, bukan hanya sekadar ditambahkan di bagian akhir.
- Jika keberhasilan atau kegagalan telah ditentukan, pintu waktu perlu menunjukkan kegagalan ketika kedaluwarsa.
- Layanan yang melakukan dekripsi yang tidak diautentikasi harus memiliki pemantauan untuk mendeteksi bahwa banjir pesan "tidak valid" telah datang.
- Perlu diingat bahwa sinyal ini membawa positif palsu (data yang rusak secara sah) dan negatif palsu (menyebarkan serangan selama waktu yang cukup lama untuk menghindari deteksi).
Menemukan kode yang rentan - aplikasi asli
Untuk program yang diimplementasikan dengan pustaka Windows Cryptography: Next Generation (CNG):
- Panggilan dekripsi adalah ke BCryptDecrypt, dengan menentukan
BCRYPT_BLOCK_PADDING
flag. - 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 untukBCRYPT_CHAINING_MODE
.
- Karena
Untuk program yang dibangun dengan Windows Cryptographic API yang lebih lama:
- Panggilan dekripsi adalah ke CryptDecrypt dengan
Final=TRUE
. - Pegangan kunci telah diinisialisasi dengan memanggil CryptSetKeyParam dengan KP_MODE disetel ke
CRYPT_MODE_CBC
.- Karena
CRYPT_MODE_CBC
adalah default, kode yang terpengaruh mungkin belum menetapkan nilai apa pun untukKP_MODE
.
- Karena
Menemukan kode yang rentan - aplikasi terkelola
- Panggilan dekripsi menuju metode CreateDecryptor() atau CreateDecryptor(Byte[], Byte[]) pada System.Security.Cryptography.SymmetricAlgorithm.
- Ini termasuk jenis turunan berikut dalam .NET, tetapi juga dapat mencakup jenis pihak ketiga:
- Properti SymmetricAlgorithm.Padding diatur ke PaddingMode.PKCS7, PaddingMode.ANSIX923, atau PaddingMode.ISO10126.
- Karena PaddingMode.PKCS7 merupakan default, kode yang terpengaruh mungkin tidak pernah menetapkan SymmetricAlgorithm.Padding properti .
- Properti SymmetricAlgorithm.Mode disetel ke CipherMode.CBC
- Karena CipherMode.CBC merupakan default, kode yang terpengaruh mungkin tidak pernah menetapkan SymmetricAlgorithm.Mode properti .
Menemukan kode yang rentan - sintaks pesan kriptografi
Pesan CMS EnvelopedData 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 sandi blok lainnya dalam mode CBC.
Meskipun cipher streaming tidak rentan terhadap kerentanan khusus ini, Microsoft merekomendasikan untuk selalu mengautentikasi data melalui pemeriksaan 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 disediakan untuk handle CMS melalui CryptMsgUpdate di mana CMSG_TYPE_PARAM yang dihasilkan adalah CMSG_ENVELOPED
dan/atau handle CMS kemudian diberikan instruksi melalui CMSG_CTRL_DECRYPT
.
Contoh kode yang rentan - dikelola
Metode ini membaca cookie dan mendekripsinya dan tidak ada pemeriksaan integritas data yang terlihat. Oleh karena itu, konten cookie yang dibaca dengan 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();
}
}
Contoh kode yang mengikuti praktik yang direkomendasikan - terkelola
Kode sampel berikut menggunakan format pesan non-standar dari
cipher_algorithm_id || hmac_algorithm_id || hmac_tag || iv || ciphertext
cipher_algorithm_id
dan hmac_algorithm_id
adalah pengidentifikasi algoritma yang merupakan representasi lokal aplikasi (tidak standar) dari algoritma tersebut. Pengidentifikasi ini mungkin lebih cocok dalam bagian lain dari protokol olahpesan yang ada alih-alih sebagai aliran byte yang digabungkan tanpa pemrosesan.
Contoh ini juga menggunakan satu kunci master untuk memperoleh kunci enkripsi dan kunci HMAC. Ini disediakan sebagai kemudahan untuk mengubah aplikasi yang menggunakan satu kunci menjadi aplikasi dengan dua kunci, dan untuk mendorong memastikan kedua kunci memiliki 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-lewat 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 pass dimungkinkan, meskipun pelaksana berhati-hati 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 (for example, 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;
}
}
}