使用填補進行 CBC 模式對稱解密的時間弱點
Microsoft 認為,除非是非常特定的情況,否則在沒有先確保加密文字的完整性下已套用可驗證填補時,使用對稱加密的加密區塊鏈結 (CBC) 模式來加密的資料已不再安全。 此判斷是以目前已知的密碼編譯研究為基礎。
簡介
填補 Oracle 攻擊是一種對加密資料的攻擊,可讓攻擊者解密資料的內容,而不需要知道金鑰。
Oracle 指的是「告知」,這可讓攻擊者知道他們正在執行的動作是否正確。 想像一下與孩子玩桌遊或卡片遊戲。 當他們的臉上堆滿著笑容,是因為他們認為自己做了傑出的一手,這就是 Oracle。 身為對手的您,可以使用此 Oracle 適當地規劃下一個動作。
填補是特定的密碼編譯詞彙。 某些加密是用來加密資料的演算法,可處理每個區塊都是固定大小的資料區塊。 如果您想要加密的資料不是填滿區塊的正確大小,則您的資料會填補到正確大小為止。 許多形式的填補都需要該填補一律存在,即使原始輸入的大小正確也一樣。 這可在解密時一律安全地移除填補。
將這兩個功能結合,具有填補 Oracle 的軟體實作會顯示解密的資料是否能有效的填補。 Oracle 可能就像傳回「不正確填補」的值一樣簡單,或者明顯花費不同時間處理有效區塊,而不是不正確區塊等更複雜的事。
區塊型加密有另一個稱之為模式的屬性,可決定第一個區塊中的資料與第二個區塊中的資料關聯性,依此類推。 其中一個最常使用的模式是 CBC。 CBC 會引進一個稱之為初始化向量 (IV) 的初始隨機區塊,並將上一個區塊與靜態加密的結果結合,使其加密相同的訊息與相同的金鑰不一定會產生相同的加密輸出。
攻擊者可以使用填補 Oracle,結合 CBC 資料的結構化方式,將稍微變更的訊息傳送至公開 Oracle 的程式碼,並持續傳送資料直到 Oracle 告知資料正確為止。 從此回應中,攻擊者可以個別位元組的方式將訊息解密。
新式電腦網路的品質如此高,攻擊者可以偵測非常小的 (少於 0.1 毫秒) 遠端系統上執行時間的差異。 假設成功解密的應用程式只有在資料未遭到竄改時才會發生,可能很容易遭受設計來觀察成功和失敗解密差異之工具的攻擊。 雖然某些語言或程式庫的時間差異可能比其他語言或程式庫更顯著,但現在普遍認為當應用程式對失敗的回應納入考慮時,這是所有語言和程式庫的實際威脅。
這種攻擊依賴變更加密資料的能力,並使用 Oracle 來測試結果。 完全減輕攻擊的唯一方法是偵測加密資料的變更,並拒絕對其執行任何動作。 這樣做的標準作法是建立資料的簽章,並在執行任何作業之前驗證簽章。 簽章必須可驗證,且攻擊者無法建立簽章,否則攻擊者會變更加密的資料,然後根據變更的資料計算新的簽章。 一種常見的適當簽章類型稱為金鑰雜湊訊息驗證碼 (HMAC)。 HMAC 與總和檢查碼不同,因為它只會讓產生 HMAC 的人員以及驗證它的人員知道秘密金鑰。 若沒有金鑰權杖,就無法產生正確的 HMAC。 當您收到資料時,您會取得加密的資料,並使用您和傳送者共用的秘密金鑰獨立計算 HMAC,然後與您計算的金鑰一同比較。 這種比較必須是固定時間,否則您可能已新增另一個可偵測的 Oracle,允許不同類型的攻擊。
總而言之,您在嘗試解密資料之前,若要安全地使用填補的 CBC 區塊加密,您必須先使用常數時間比較將它們與您驗證的 HMAC (或另一個資料完整性檢查) 結合。 由於所有已改變的訊息都會花費相同的時間來產生回應,因此會避免攻擊。
易受攻擊的對象
此弱點同時適用於執行其自身之加密和解密的受控和原生應用程式。 其中包含,例如:
- 在伺服器上加密 Cookie 以供日後解密的應用程式。
- 可讓使用者將資料插入稍後其資料行將解密之資料表的資料庫應用程式。
- 依賴使用共用金鑰來保護傳輸中資料之加密的資料傳輸應用程式。
- 加密和解密訊息在 TLS 通道「內」的應用程式。
請注意,單獨使用 TLS 可能無法在這些案例中保護您。
易受攻擊的應用程式:
- 使用 CBC 加密模式搭配可驗證填補模式來解密資料,例如 PKCS#7 或 ANSI X.923。
- 可執行解密,而不需執行資料完整性檢查 (透過 MAC 或非對稱數位簽章)。
這也適用於建置於這些基本類型上之抽象概念的應用程式,例如密碼編譯訊息語法 (PKCS#7/CMS) EnvelopedData 結構。
相關考量的區域
當訊息具有已知或可預測的頁尾結構時,研究使得 Microsoft 進一步關注使用 ISO 10126 對等填補所填補的 CBC 訊息。 例如,根據 W3C XML 加密語法和處理建議之規則 (xmlenc 和 EncryptedXml) 準備的內容。 雖然 W3C 指引在簽署訊息的當下視為適當加密,但 Microsoft 現在建議一律執行加密後簽署。
應用程式開發人員應該一律留意驗證非對稱簽章金鑰的適用性,因為非對稱金鑰與任意訊息之間沒有固有的信任關係。
詳細資料
以往,使用 HMAC 或 RSA 簽章等方式加密和驗證重要資料的共識非常重要。 不過,對於如何排序加密和驗證作業的方式,指引較不清楚。 由於本文詳述的弱點,Microsoft 的指引現在一律使用「加密後簽署」範例。 也就是說,請先使用對稱金鑰加密資料,然後透過加密文字 (加密的資料) 計算 MAC 或非對稱簽章。 解密資料時,請執行反向作業。 首先,確認加密文字的 MAC 或簽章,然後將其解密。
稱為「填補 Oracle 攻擊」的弱點類別已知存在超過 10 年。 這些弱點可讓攻擊者解密由對稱封鎖演算法加密的資料 (例如 AES 和 3DES),且每個資料區塊的嘗試次數不超過 4096 次。 這些弱點會利用區塊加密最常在結尾與可驗證填補資料搭配使用的事實。 經發現,如果攻擊者可以竄改加密文字,並找出竄改是否在結尾的填補格式造成錯誤,攻擊者就可以解密資料。
一開始,實際攻擊是以會根據填補是否有效傳回不同錯誤碼的服務為基礎,例如 ASP.NET 弱點 MS10-070。 不過,Microsoft 現在認為只使用處理有效和不正確填補之間的時間差異,進行類似的攻擊是可行的。
如果加密配置採用簽章,而且簽章驗證是以指定長度的資料之固定執行時間執行 (不論內容為何),都可以驗證資料完整性,不需要透過側邊通道向攻擊者發出任何資訊。 由於完整性檢查會拒絕任何遭竄改的訊息,因此填補 Oracle 威脅會降低。
指引
首先,Microsoft 建議任何具有機密性需求的資料都必須透過傳輸層安全性 (TLS) 傳輸,這是安全通訊端層 (SSL) 的後續任務。
接下來,分析您的應用程式以:
- 精確地瞭解您執行的加密,以及您使用的平台和 API 所提供的加密。
- 請確定在 CBC 模式中,對稱區塊加密演算法的每一層使用方式 (例如 AES 和 3DES) 會併入秘密金鑰資料完整性檢查的使用 (非對稱簽章、HMAC 或將加密模式變更為已驗證的加密 (AE) 模式,例如 GCM 或 CCM)。
根據目前的研究,一般認為針對非 AE 加密模式獨立執行驗證和加密步驟時,驗證加密文字 (加密後簽署) 是最佳的一般選項。 不過,密碼編譯沒有一體適用的正確答案,而且這種一般化的答案不如專業密碼編譯工具的直接建議來得有用。
鼓勵無法變更其傳訊格式但執行未經驗證 CBC 解密的應用程式嘗試納入風險降低,例如:
- 未允許解密程式驗證或移除填補的解密:
- 套用的任何填補仍然需要移除或忽略,您會將負擔移至您的應用程式。
- 優點是填補驗證和移除可以併入其他應用程式資料驗證邏輯。 如果填補驗證和資料驗證可以在固定時間內完成,就會減少威脅。
- 由於填補的解譯會變更所認知的訊息長度,因此可能仍有從此方法發出的時間資訊。
- 將解密填補模式變更為 ISO10126:
- ISO10126 解密填補與 PKCS7 加密填補和 ANSIX923 加密填補相容。
- 變更模式會將填補 Oracle 知識縮減為 1 個位元組,而不是整個區塊。 不過,如果內容具有已知的頁尾 (例如結尾 XML 元素),相關攻擊可以繼續攻擊訊息的其餘部分。
- 在攻擊者可能會強制使用相同的純文字多次以不同的訊息位移加密的情況下,這也不會避免純文字復原。
- 控制解密呼叫的評估,以降低時間訊號:
- 保留時間的計算必須超過解密作業針對包含填補的任何資料區段所花費的時間上限。
- 時間計算應該根據取得高解析度時間戳記中的指引來完成,而不是使用 Environment.TickCount (受限於變換/溢位) 或是減去兩個系統時間戳記 (受限於 NTP 調整錯誤) 來完成。
- 時間計算必須包含解密作業,包括受控或 C++ 應用程式中的所有潛在例外狀況,而不只是填補到結尾。
- 如果已判斷成功或失敗,時間閘道必須在失敗到期時傳回失敗。
- 執行未經驗證解密的服務應該就地進行監視,以偵測到「不正確」訊息的溢流已通過。
- 請記住,此訊號同時具有誤判為真 (合法損毀的資料) 和誤判為否 (將攻擊分散到足夠的時間,以規避偵測)。
尋找易受攻擊的程式碼:原生應用程式
針對 Windows 密碼編譯建置的程式:新一代 (CNG) 程式庫:
- 解密呼叫是 BCryptDecrypt,並指定
BCRYPT_BLOCK_PADDING
旗標。 - 已藉由呼叫 BCryptSetProperty 並將 BCRYPT_CHAINING_MODE 設為
BCRYPT_CHAIN_MODE_CBC
來初始化金鑰控制碼。- 因為
BCRYPT_CHAIN_MODE_CBC
是預設值,所以受影響的程式碼可能未指派BCRYPT_CHAINING_MODE
的任何值。
- 因為
針對舊版 Windows 密碼編譯 API 建置的程式:
- 解密呼叫是使用
Final=TRUE
的 CryptDecrypt。 - 已藉由呼叫 CryptSetKeyParam,並將 KP_MODE 設定為
CRYPT_MODE_CBC
來初始化金鑰控制代碼。- 因為
CRYPT_MODE_CBC
是預設值,所以受影響的程式碼可能未指派KP_MODE
的任何值。
- 因為
尋找易受攻擊的程式碼:受控應用程式
- 解密呼叫是 System.Security.Cryptography.SymmetricAlgorithm 上的 CreateDecryptor() 或 CreateDecryptor(Byte[], Byte[]) 方法。
- 這包括 .NET 中的下列衍生類型,但也可能包含協力廠商類型:
- 系統將 SymmetricAlgorithm.Padding 屬性設定為 PaddingMode.PKCS7、PaddingMode.ANSIX923 或 PaddingMode.ISO10126。
- 由於 PaddingMode.PKCS7 是預設值,因此受影響的程式碼可能從未指派 SymmetricAlgorithm.Padding 屬性。
- 系統將 SymmetricAlgorithm.Mode 屬性設定為 CipherMode.CBC
- 由於 CipherMode.CBC 是預設值,因此受影響的程式碼可能從未指派 SymmetricAlgorithm.Mode 屬性。
尋找易受攻擊的程式碼:密碼編譯訊息語法
未經驗證的 CMS EnvelopedData 訊息,其加密內容使用 AES 的 CBC 模式 (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 的 CBC 模式 (1.3.14.3.2.7)、3DES 的 CBC 模式 (1.2.840.113549.3.7) 或 RC2 的 CBC 模式 (1.2.840.113549.3.2) 易受攻擊,以及使用 CBC 模式中任何其他區塊加密演算法的訊息。
雖然串流加密不容易受到此特定弱點的影響,但 Microsoft 建議一律驗證資料,以檢查 ContentEncryptionAlgorithm 值。
對於受控應用程式,CMS EnvelopedData blob 可以偵測為傳遞至 System.Security.Cryptography.Pkcs.EnvelopedCms.Decode(Byte[]) 的任何值。
針對原生應用程式,CMS EnvelopedData blob 可以偵測為透過 CryptMsgUpdate 提供給 CMS 控制代碼的任何值,其產生的 CMSG_TYPE_PARAM 為 CMSG_ENVELOPED
,及/或 CMS 控制代碼稍後會透過 CryptMsgControl 傳送 CMSG_CTRL_DECRYPT
指令。
易受攻擊的程式碼範例:受控
這個方法會讀取 Cookie 並將其解密,而且不會經過資料完整性檢查。 因此,此方法所讀取的 Cookie 內容可能會受到接收該 Cookie 的使用者攻擊,或由已取得加密 Cookie 值的任何攻擊者攻擊。
private byte[] DecryptCookie(string cookieName)
{
HttpCookie cookie = Request.Cookies[cookieName];
if (cookie == null)
{
return null;
}
using (ICryptoTransform decryptor = _aes.CreateDecryptor())
using (MemoryStream memoryStream = new MemoryStream())
using (CryptoStream cryptoStream =
new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Write))
{
byte[] readCookie = Convert.FromBase64String(cookie.Value);
cryptoStream.Write(readCookie, 0, readCookie.Length);
cryptoStream.FlushFinalBlock();
return memoryStream.ToArray();
}
}
遵循建議做法的範例程式碼:受控
下列範例程式碼使用非標準訊息格式來自
cipher_algorithm_id || hmac_algorithm_id || hmac_tag || iv || ciphertext
其中 cipher_algorithm_id
和 hmac_algorithm_id
演算法識別碼是這些演算法的應用程式本機 (非標準) 標記法。 這些識別碼在現有傳訊通訊協定的其他部分可能很合理,作為裸機串連的位元組資料流則不合理。
此範例也會使用單一主要金鑰來衍生加密金鑰和 HMAC 金鑰。 這兩者都是為了方便將單向金鑰應用程式轉換成雙向金鑰應用程式,並鼓勵將兩個金鑰保留為不同的值。 它進一步保證 HMAC 金鑰和加密金鑰無法脫離同步處理。
此範例不接受加密或解密的 Stream。 目前的資料格式會使單次加密變得困難,因為 hmac_tag
值在加密文字之前。 不過,已選擇此格式,因為它會保留開頭的所有固定大小元素,讓剖析器保持更簡單。 透過此資料格式,可以進行單次解密,不過系統會提醒實作者呼叫 GetHashAndReset,並在呼叫 TransformFinalBlock 之前驗證結果。 如果串流加密很重要,則可能需要不同的 AE 模式。
// ==++==
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// Shared under the terms of the Microsoft Public License,
// https://opensource.org/licenses/MS-PL
//
// ==--==
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
namespace Microsoft.Examples.Cryptography
{
public enum AeCipher : byte
{
Unknown,
Aes256CbcPkcs7,
}
public enum AeMac : byte
{
Unknown,
HMACSHA256,
HMACSHA384,
}
/// <summary>
/// Provides extension methods to make HashAlgorithm look like .NET Core's
/// IncrementalHash
/// </summary>
internal static class IncrementalHashExtensions
{
public static void AppendData(this HashAlgorithm hash, byte[] data)
{
hash.TransformBlock(data, 0, data.Length, null, 0);
}
public static void AppendData(
this HashAlgorithm hash,
byte[] data,
int offset,
int length)
{
hash.TransformBlock(data, offset, length, null, 0);
}
public static byte[] GetHashAndReset(this HashAlgorithm hash)
{
hash.TransformFinalBlock(Array.Empty<byte>(), 0, 0);
return hash.Hash;
}
}
public static partial class AuthenticatedEncryption
{
/// <summary>
/// Use <paramref name="masterKey"/> to derive two keys (one cipher, one HMAC)
/// to provide authenticated encryption for <paramref name="message"/>.
/// </summary>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <param name="message">The message to encrypt</param>
/// <returns>
/// A concatenation of
/// [cipher algorithm+chainmode+padding][mac algorithm][authtag][IV][ciphertext],
/// suitable to be passed to <see cref="Decrypt"/>.
/// </returns>
/// <remarks>
/// <paramref name="masterKey"/> should be a 128-bit (or bigger) value generated
/// by a secure random number generator, such as the one returned from
/// <see cref="RandomNumberGenerator.Create()"/>.
/// This implementation chooses to block deficient inputs by length, but does not
/// make any attempt at discerning the randomness of the key.
///
/// If the master key is being input by a prompt (like a password/passphrase)
/// then it should be properly turned into keying material via a Key Derivation
/// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
/// never be simply turned to bytes via an Encoding class and used as a key.
/// </remarks>
public static byte[] Encrypt(byte[] masterKey, byte[] message)
{
if (masterKey == null)
throw new ArgumentNullException(nameof(masterKey));
if (masterKey.Length < 16)
throw new ArgumentOutOfRangeException(
nameof(masterKey),
"Master Key must be at least 128 bits (16 bytes)");
if (message == null)
throw new ArgumentNullException(nameof(message));
// First, choose an encryption scheme.
AeCipher aeCipher = AeCipher.Aes256CbcPkcs7;
// Second, choose an authentication (message integrity) scheme.
//
// In this example we use the master key length to change from HMACSHA256 to
// HMACSHA384, but that is completely arbitrary. This mostly represents a
// "cryptographic needs change over time" scenario.
AeMac aeMac = masterKey.Length < 48 ? AeMac.HMACSHA256 : AeMac.HMACSHA384;
// It's good to be able to identify what choices were made when a message was
// encrypted, so that the message can later be decrypted. This allows for
// future versions to add support for new encryption schemes, but still be
// able to read old data. A practice known as "cryptographic agility".
//
// This is similar in practice to PKCS#7 messaging, but this uses a
// private-scoped byte rather than a public-scoped Object IDentifier (OID).
// Please note that the scheme in this example adheres to no particular
// standard, and is unlikely to survive to a more complete implementation in
// the .NET Framework.
//
// You may be well-served by prepending a version number byte to this
// message, but may want to avoid the value 0x30 (the leading byte value for
// DER-encoded structures such as X.509 certificates and PKCS#7 messages).
byte[] algorithmChoices = { (byte)aeCipher, (byte)aeMac };
byte[] iv;
byte[] cipherText;
byte[] tag;
// Using our algorithm choices, open an HMAC (as an authentication tag
// generator) and a SymmetricAlgorithm which use different keys each derived
// from the same master key.
//
// A custom implementation may very well have distinctly managed secret keys
// for the MAC and cipher, this example merely demonstrates the master to
// derived key methodology to encourage key separation from the MAC and
// cipher keys.
using (HMAC tagGenerator = GetMac(aeMac, masterKey))
{
using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
using (ICryptoTransform encryptor = cipher.CreateEncryptor())
{
// Since no IV was provided, a random one has been generated
// during the call to CreateEncryptor.
//
// But note that it only does the auto-generation once. If the cipher
// object were used again, a call to GenerateIV would have been
// required.
iv = cipher.IV;
cipherText = Transform(encryptor, message, 0, message.Length);
}
// The IV and ciphertext both need to be included in the MAC to prevent
// tampering.
//
// By including the algorithm identifiers, we have technically moved from
// simple Authenticated Encryption (AE) to Authenticated Encryption with
// Additional Data (AEAD). By including the algorithm identifiers in the
// MAC, it becomes harder for an attacker to change them as an attempt to
// perform a downgrade attack.
//
// If you've added a data format version field, it can also be included
// in the MAC to further inhibit an attacker's options for confusing the
// data processor into believing the tampered message is valid.
tagGenerator.AppendData(algorithmChoices);
tagGenerator.AppendData(iv);
tagGenerator.AppendData(cipherText);
tag = tagGenerator.GetHashAndReset();
}
// Build the final result as the concatenation of everything except the keys.
int totalLength =
algorithmChoices.Length +
tag.Length +
iv.Length +
cipherText.Length;
byte[] output = new byte[totalLength];
int outputOffset = 0;
Append(algorithmChoices, output, ref outputOffset);
Append(tag, output, ref outputOffset);
Append(iv, output, ref outputOffset);
Append(cipherText, output, ref outputOffset);
Debug.Assert(outputOffset == output.Length);
return output;
}
/// <summary>
/// Reads a message produced by <see cref="Encrypt"/> after verifying it hasn't
/// been tampered with.
/// </summary>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <param name="cipherText">
/// The output of <see cref="Encrypt"/>: a concatenation of a cipher ID, mac ID,
/// authTag, IV, and cipherText.
/// </param>
/// <returns>The decrypted content.</returns>
/// <exception cref="ArgumentNullException">
/// <paramref name="masterKey"/> is <c>null</c>.
/// </exception>
/// <exception cref="ArgumentNullException">
/// <paramref name="cipherText"/> is <c>null</c>.
/// </exception>
/// <exception cref="CryptographicException">
/// <paramref name="cipherText"/> identifies unknown algorithms, is not long
/// enough, fails a data integrity check, or fails to decrypt.
/// </exception>
/// <remarks>
/// <paramref name="masterKey"/> should be a 128-bit (or larger) value
/// generated by a secure random number generator, such as the one returned from
/// <see cref="RandomNumberGenerator.Create()"/>. This implementation chooses to
/// block deficient inputs by length, but doesn't make any attempt at
/// discerning the randomness of the key.
///
/// If the master key is being input by a prompt (like a password/passphrase),
/// then it should be properly turned into keying material via a Key Derivation
/// Function like PBKDF2, represented by Rfc2898DeriveBytes. A 'password' should
/// never be simply turned to bytes via an Encoding class and used as a key.
/// </remarks>
public static byte[] Decrypt(byte[] masterKey, byte[] cipherText)
{
// This example continues the .NET practice of throwing exceptions for
// failures. If you consider message tampering to be normal (and thus
// "not exceptional") behavior, you may like the signature
// bool Decrypt(byte[] messageKey, byte[] cipherText, out byte[] message)
// better.
if (masterKey == null)
throw new ArgumentNullException(nameof(masterKey));
if (masterKey.Length < 16)
throw new ArgumentOutOfRangeException(
nameof(masterKey),
"Master Key must be at least 128 bits (16 bytes)");
if (cipherText == null)
throw new ArgumentNullException(nameof(cipherText));
// The format of this message is assumed to be public, so there's no harm in
// saying ahead of time that the message makes no sense.
if (cipherText.Length < 2)
{
throw new CryptographicException();
}
// Use the message algorithm headers to determine what cipher algorithm and
// MAC algorithm are going to be used. Since the same Key Derivation
// Functions (KDFs) are being used in Decrypt as Encrypt, the keys are also
// the same.
AeCipher aeCipher = (AeCipher)cipherText[0];
AeMac aeMac = (AeMac)cipherText[1];
using (SymmetricAlgorithm cipher = GetCipher(aeCipher, masterKey))
using (HMAC tagGenerator = GetMac(aeMac, masterKey))
{
int blockSizeInBytes = cipher.BlockSize / 8;
int tagSizeInBytes = tagGenerator.HashSize / 8;
int headerSizeInBytes = 2;
int tagOffset = headerSizeInBytes;
int ivOffset = tagOffset + tagSizeInBytes;
int cipherTextOffset = ivOffset + blockSizeInBytes;
int cipherTextLength = cipherText.Length - cipherTextOffset;
int minLen = cipherTextOffset + blockSizeInBytes;
// Again, the minimum length is still assumed to be public knowledge,
// nothing has leaked out yet. The minimum length couldn't just be calculated
// without reading the header.
if (cipherText.Length < minLen)
{
throw new CryptographicException();
}
// It's very important that the MAC be calculated and verified before
// proceeding to decrypt the ciphertext, as this prevents any sort of
// information leaking out to an attacker.
//
// Don't include the tag in the calculation, though.
// First, everything before the tag (the cipher and MAC algorithm ids)
tagGenerator.AppendData(cipherText, 0, tagOffset);
// Skip the data before the tag and the tag, then read everything that
// remains.
tagGenerator.AppendData(
cipherText,
tagOffset + tagSizeInBytes,
cipherText.Length - tagSizeInBytes - tagOffset);
byte[] generatedTag = tagGenerator.GetHashAndReset();
// The time it took to get to this point has so far been a function only
// of the length of the data, or of non-encrypted values (e.g. it could
// take longer to prepare the *key* for the HMACSHA384 MAC than the
// HMACSHA256 MAC, but the algorithm choice wasn't a secret).
//
// If the verification of the authentication tag aborts as soon as a
// difference is found in the byte arrays then your program may be
// acting as a timing oracle which helps an attacker to brute-force the
// right answer for the MAC. So, it's very important that every possible
// "no" (2^256-1 of them for HMACSHA256) be evaluated in as close to the
// same amount of time as possible. For this, we call CryptographicEquals
if (!CryptographicEquals(
generatedTag,
0,
cipherText,
tagOffset,
tagSizeInBytes))
{
// Assuming every tampered message (of the same length) took the same
// amount of time to process, we can now safely say
// "this data makes no sense" without giving anything away.
throw new CryptographicException();
}
// Restore the IV into the symmetricAlgorithm instance.
byte[] iv = new byte[blockSizeInBytes];
Buffer.BlockCopy(cipherText, ivOffset, iv, 0, iv.Length);
cipher.IV = iv;
using (ICryptoTransform decryptor = cipher.CreateDecryptor())
{
return Transform(
decryptor,
cipherText,
cipherTextOffset,
cipherTextLength);
}
}
}
private static byte[] Transform(
ICryptoTransform transform,
byte[] input,
int inputOffset,
int inputLength)
{
// Many of the implementations of ICryptoTransform report true for
// CanTransformMultipleBlocks, and when the entire message is available in
// one shot this saves on the allocation of the CryptoStream and the
// intermediate structures it needs to properly chunk the message into blocks
// (since the underlying stream won't always return the number of bytes
// needed).
if (transform.CanTransformMultipleBlocks)
{
return transform.TransformFinalBlock(input, inputOffset, inputLength);
}
// If our transform couldn't do multiple blocks at once, let CryptoStream
// handle the chunking.
using (MemoryStream messageStream = new MemoryStream())
using (CryptoStream cryptoStream =
new CryptoStream(messageStream, transform, CryptoStreamMode.Write))
{
cryptoStream.Write(input, inputOffset, inputLength);
cryptoStream.FlushFinalBlock();
return messageStream.ToArray();
}
}
/// <summary>
/// Open a properly configured <see cref="SymmetricAlgorithm"/> conforming to the
/// scheme identified by <paramref name="aeCipher"/>.
/// </summary>
/// <param name="aeCipher">The cipher mode to open.</param>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <returns>
/// A SymmetricAlgorithm object with the right key, cipher mode, and padding
/// mode; or <c>null</c> on unknown algorithms.
/// </returns>
private static SymmetricAlgorithm GetCipher(AeCipher aeCipher, byte[] masterKey)
{
SymmetricAlgorithm symmetricAlgorithm;
switch (aeCipher)
{
case AeCipher.Aes256CbcPkcs7:
symmetricAlgorithm = Aes.Create();
// While 256-bit, CBC, and PKCS7 are all the default values for these
// properties, being explicit helps comprehension more than it hurts
// performance.
symmetricAlgorithm.KeySize = 256;
symmetricAlgorithm.Mode = CipherMode.CBC;
symmetricAlgorithm.Padding = PaddingMode.PKCS7;
break;
default:
// An algorithm we don't understand
throw new CryptographicException();
}
// Instead of using the master key directly, derive a key for our chosen
// HMAC algorithm based upon the master key.
//
// Since none of the symmetric encryption algorithms currently in .NET
// support key sizes greater than 256-bit, we can use HMACSHA256 with
// NIST SP 800-108 5.1 (Counter Mode KDF) to derive a value that is
// no smaller than the key size, then Array.Resize to trim it down as
// needed.
using (HMAC hmac = new HMACSHA256(masterKey))
{
// i=1, Label=ASCII(cipher)
byte[] cipherKey = hmac.ComputeHash(
new byte[] { 1, 99, 105, 112, 104, 101, 114 });
// Resize the array to the desired keysize. KeySize is in bits,
// and Array.Resize wants the length in bytes.
Array.Resize(ref cipherKey, symmetricAlgorithm.KeySize / 8);
symmetricAlgorithm.Key = cipherKey;
}
return symmetricAlgorithm;
}
/// <summary>
/// Open a properly configured <see cref="HMAC"/> conforming to the scheme
/// identified by <paramref name="aeMac"/>.
/// </summary>
/// <param name="aeMac">The message authentication mode to open.</param>
/// <param name="masterKey">The master key from which other keys derive.</param>
/// <returns>
/// An HMAC object with the proper key, or <c>null</c> on unknown algorithms.
/// </returns>
private static HMAC GetMac(AeMac aeMac, byte[] masterKey)
{
HMAC hmac;
switch (aeMac)
{
case AeMac.HMACSHA256:
hmac = new HMACSHA256();
break;
case AeMac.HMACSHA384:
hmac = new HMACSHA384();
break;
default:
// An algorithm we don't understand
throw new CryptographicException();
}
// Instead of using the master key directly, derive a key for our chosen
// HMAC algorithm based upon the master key.
// Since the output size of the HMAC is the same as the ideal key size for
// the HMAC, we can use the master key over a fixed input once to perform
// NIST SP 800-108 5.1 (Counter Mode KDF):
hmac.Key = masterKey;
// i=1, Context=ASCII(MAC)
byte[] newKey = hmac.ComputeHash(new byte[] { 1, 77, 65, 67 });
hmac.Key = newKey;
return hmac;
}
// A simple helper method to ensure that the offset (writePos) always moves
// forward with new data.
private static void Append(byte[] newData, byte[] combinedData, ref int writePos)
{
Buffer.BlockCopy(newData, 0, combinedData, writePos, newData.Length);
writePos += newData.Length;
}
/// <summary>
/// Compare the contents of two arrays in an amount of time which is only
/// dependent on <paramref name="length"/>.
/// </summary>
/// <param name="a">An array to compare to <paramref name="b"/>.</param>
/// <param name="aOffset">
/// The starting position within <paramref name="a"/> for comparison.
/// </param>
/// <param name="b">An array to compare to <paramref name="a"/>.</param>
/// <param name="bOffset">
/// The starting position within <paramref name="b"/> for comparison.
/// </param>
/// <param name="length">
/// The number of bytes to compare between <paramref name="a"/> and
/// <paramref name="b"/>.</param>
/// <returns>
/// <c>true</c> if both <paramref name="a"/> and <paramref name="b"/> have
/// sufficient length for the comparison and all of the applicable values are the
/// same in both arrays; <c>false</c> otherwise.
/// </returns>
/// <remarks>
/// An "insufficient data" <c>false</c> response can happen early, but otherwise
/// a <c>true</c> or <c>false</c> response take the same amount of time.
/// </remarks>
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
private static bool CryptographicEquals(
byte[] a,
int aOffset,
byte[] b,
int bOffset,
int length)
{
Debug.Assert(a != null);
Debug.Assert(b != null);
Debug.Assert(length >= 0);
int result = 0;
if (a.Length - aOffset < length || b.Length - bOffset < length)
{
return false;
}
unchecked
{
for (int i = 0; i < length; i++)
{
// Bitwise-OR of subtraction has been found to have the most
// stable execution time.
//
// This cannot overflow because bytes are 1 byte in length, and
// result is 4 bytes.
// The OR propagates all set bytes, so the differences are only
// present in the lowest byte.
result = result | (a[i + aOffset] - b[i + bOffset]);
}
}
return result == 0;
}
}
}