[譯]最佳安全實踐:在 Java 和 Android 中使用 AES 進行對稱加密:第2部分:AES-CBC + HMAC

原文地址:Security Best Practices: Symmetric Encryption with AES in Java and Android: Part 2: AES-CBC + HMACphp

本文是我上一篇文章:「最佳安全實踐:在 Java 和 Android 中使用 AES 進行對稱加密」 的續篇,在這篇文章中我總結了關於 AES 最爲重要的事情並演示瞭如何經過 AES-GCM 來使用它。在閱讀本文並深刻下一個主題以前,我強烈建議你閱讀它,由於它解釋了最重要的基礎知識。html


本文討論瞭如下可能發生的狀況:你不能經過相似 Galois/Counter Mode (GCM) 的認證加密模式來使用高級加密標準(AES)?你當前使用的平臺不支持它,或者你必須兼容老版本或其它第三方協議?不管你放棄 GCM 的緣由是什麼,你都不該該放棄它所具備的安全屬性:java

  • 保密性:沒有密鑰的人沒法閱讀該消息
  • 完整性:沒有人會修改消息內容
  • 真實性:能夠對消息的發送者進行驗證

選擇非認證加密,好比塊模式密碼分組連接(CBC),不幸的是,因爲具有很好的延展性,它缺乏後兩個安全屬性。如何解決這個問題?正如我在上一篇文章中所說的那樣,一種可能的解決方案是將加密原語組合在一塊兒以包含加密驗證碼(MAC)android

加密驗證碼(MAC)

那麼什麼是 MAC,咱們爲何要使用它呢?MAC 相似於散列函數,這意味着它將消息做爲輸入並生成一個所謂的簡短標記。爲了確保並不是任何人均可覺得任意消息建立標記,MAC 函數須要一個密鑰來進行計算。與使用非對稱加密的簽名相比,MAC 可以使用相同的密鑰來進行標記生成和認證。git

例如,若是雙方安全地交換了 MAC 密鑰,而且每條消息都附加了認證標記,那麼它們均可以檢查消息是不是由另外一方建立的,而且在傳輸過程當中沒有被更改。攻擊者須要保密的 MAC 密鑰來僞造身份進行標記驗證。github

最普遍使用的 MAC 類型之一是 散列消息密鑰驗證碼(HMAC),它包含一個哈希加密函數,該函數一般是 SHA256。因爲我不會詳細介紹其算法,所以我建議你閱讀相關 RFC。固然還有如 CBC-MAC 等其餘可用於對稱加密的類型。幾乎全部的加密框架都至少包含一個 HMAC 實現,包括經過 Mac 實現的 JCA/JCE算法

使用加密的 MAC:架構

那麼正確應用 MAC 的方法是什麼呢?根據安全研究院 Hugo Krawcyzk 的說法,這裏有三種基本選項apache

  • MAC-then-Encrypt:基於明文生成 MAC,而後將其追加到明文中後再進行加密(在 SSL 中使用)
  • Encrypt-then-MAC:基於密文和初始向量生成 MAC,而後將其追加到密文中(在 IPsec 中使用)
  • Encrypt-and-MAC: 基於明文生成 MAC、而後將其追加到密文中(在 SSH 中使用)

每個選項都有它本身的屬性,我建議你經過這篇文章來獲取每一個選項的完整參數。總而言之,大部分 研究員 推薦使用 Encrypt-then-MAC(EtM)。因爲 MAC 能夠防止不正確消息的解密,它能夠防止選擇密文攻擊。此外也因爲 MAC 在密文中運行,它不能泄漏有關明文的任何信息。然而它的缺點是,由於 IV 和標記中必須包含可能的協議/算法版本或類型,所以實施起來稍微有些困難。重要的是在驗證 MAC 以前永遠不要進行任何加密操做,不然你可能受到 padding-oracle 攻擊Moxie 稱之爲末日原則)。api

Encrypt-then-Mac 架構

Encrypt-then-Mac 架構

附錄:CGM 和 Encrypt-then-MAC 一般狀況下它們的安全強度可能相似,CGM 有如下優勢:數組

  • 簡單易用而不易出錯
  • 更快,由於它只須要一次經過整個信息

它的缺點是隻能容許 96 位初始向量(對於 128 位),HMAC 理論上比 GCM 的內部 MAC 算法 GHASH(128 位標記大小對 256 位及以上)更強。GCM 沒法進行 IV + 密鑰重用。相關詳細討論,請查閱此處

使用加密的 MAC:驗證密鑰

咱們必須解決的最後一個問題是:咱們應該從哪裏得到用於 MAC 計算的密鑰?若是使用的是強密鑰(即足夠隨機且能夠安全地切換),那麼使用與加密相同的密鑰(當使用 HMAC 時)彷佛沒有已知問題。但最佳實踐是使用密鑰派生函數(KDF)派生出 2 個子密鑰以防範將來可能發現的任何問題。這能夠像計算主密鑰上的 SHA256 並將其拆分爲兩個 16 字節塊同樣簡單。 可是我更喜歡標準化的協議,好比基於 HMAC 的 Extract-and-Expand 密鑰派生函數,它直接支持此場景而不須要字節調整。

2 個子密鑰的派生

2 個子密鑰的派生

在 Java 和 Android 中使用 EtM 實現 AES-CBC

理論已經足夠了,如今讓咱們開始編碼!在接下來的例子中,我將使用 AES-CBC,這是一個看似保守的決定。這樣作的緣由是,應該保證幾乎每一個 JREAndroid 版本均可以使用它。如前所述,咱們將使用帶有 HMAC 的 Encrypt-then-Mac 方案。這裏惟一的外部依賴是 HKDF。這段代碼基本上是我在上一篇文章中描述的 GCM 示例的一個映射。

加密

簡單起見,咱們使用隨機生成的 128 位密鑰。當你傳遞 12八、192 或 256 位長度的密鑰時,Java 將自動選擇正確的模式。但請注意,256 位加密一般須要在 JRE 中安裝 無政策限制權限文件(OpenJDK 和 Android 無需安裝)。若是你不肯定要使用的密鑰大小,請在個人上一篇文章中閱讀關於該主題的相關段落。

SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);
複製代碼

而後咱們須要建立咱們的初始化向量。對於 CBC,應該使用 16 個字節長的初始向量(IV)。請注意,始終使用像 SecureRandom 這樣的強僞隨機數生成器(PRNG)

byte[] iv = new byte[16];
secureRandom.nextBytes(iv);
複製代碼

重用 IV 不像 GCM 那樣具備災難性,但最好仍是避免使用。在這裏能夠看到可能的攻擊

下一步,咱們將派生出加密和身份驗證所需的 2 個子密鑰。咱們將在配置 HMAC-SHA256(使用此庫)中使用 HKDF,因爲它使用起來簡單直接。咱們使用 HKDF 中的 info 參數來生成兩個 16 字節子密鑰,從而對它們進行區分。

// import at.favre.lib.crypto.HKDF;
byte[] encKey = HKDF.fromHmacSha256().expand(key, "encKey".getBytes(StandardCharsets.UTF_8), 16);
byte[] authKey = HKDF.fromHmacSha256().expand(key, "authKey".getBytes(StandardCharsets.UTF_8), 32); //HMAC-SHA256 key is 32 byte
複製代碼

接下來,咱們將初始化密碼並加密咱們的明文。因爲 CBC 的行爲相似於塊模式,所以咱們須要一個填充模式用於填充不徹底符合 16 字節塊大小的信息。因爲對使用的填充方案彷佛沒有安全隱患,咱們選擇了支持最普遍的:PKCS#7

注意: 因爲歷史緣由,咱們必須將密碼套件設置爲 PKCS5。除了被定義爲了避免同的塊尺寸,二者幾乎徹底相同;一般狀況下 PKCS#5 與 AES 並不兼容,但因爲定義可追溯到使用了 8 字節塊的 3DES,咱們堅持使用它。若是你的 JCE 提供程序接受 AES/CBC/PKCS7Padding,那麼使用此定義更好,如此你的代碼將更容易被理解。

final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //actually uses PKCS#7
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encKey, "AES"), new IvParameterSpec(iv));
byte[] cipherText = cipher.doFinal(plainText);
複製代碼

接下來,咱們須要準備 MAC 並添加主要數據來進行身份驗證。

SecretKey macKey = new SecretKeySpec(authKey, "HmacSHA256");
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(macKey);
hmac.update(iv);
hmac.update(cipherText);
複製代碼

若是你想要驗證其餘元數據(好比協議版本),你還能夠將其添加到 mac 生成過程當中。這與將關聯數據添加到通過身份驗證的加密算法的概念相同。

if (associatedData != null) {
    hmac.update(associatedData);
}
複製代碼

而後計算 mac:

byte[] mac = hmac.doFinal();
複製代碼

最後將全部信息序列化爲單個消息:

ByteBuffer byteBuffer = ByteBuffer.allocate(1 + iv.length + 1 + mac.length + cipherText.length);
byteBuffer.put((byte) iv.length);
byteBuffer.put(iv);
byteBuffer.put((byte) mac.length);
byteBuffer.put(mac);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();
複製代碼

這基本上就是加密。將構建消息、IV、IV 的長度以及 mac 的長度、mac 和加密數據附加到單個字節數組。

若是你須要字符串表示,能夠選用 Base64 對其進行編碼。Android 中有該編碼的標準實現,JDK 僅從版本 8 開始支持(若是可能,我將避免使用 Apache Commons Codec,由於它很慢且實現混亂)。

因爲 Java 是一種自動內存管理語言,所以最佳作法是儘量快地從內存中擦除加密密鑰或 IV 等敏感數據。咱們沒法保證如下內容可以按照預期工做,但在大多數狀況下應該如此:

Arrays.fill(authKey, (byte) 0);
Arrays.fill(encKey, (byte) 0);
複製代碼

注意不要覆蓋還在其餘地方使用的數據。

解密

解密和反向加密相似:首先解構消息。

ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);

int ivLength = (byteBuffer.get());
if (ivLength != 16) { // check input parameter
    throw new IllegalArgumentException("invalid iv length");
}
byte[] iv = new byte[ivLength];
byteBuffer.get(iv);

int macLength = (byteBuffer.get());
if (macLength != 32) { // check input parameter
    throw new IllegalArgumentException("invalid mac length");
}
byte[] mac = new byte[macLength];
byteBuffer.get(mac);

byte[] cipherText = new byte[byteBuffer.remaining()];
byteBuffer.get(cipherText);
複製代碼

仔細驗證輸入參數以防止拒絕服務攻擊,如 IV 或 mac 長度,由於攻擊者可能會更改相關值。

而後導出解密和身份驗證所需的密鑰。

// import at.favre.lib.crypto.HKDF;
byte[] encKey = HKDF.fromHmacSha256().expand(key, "encKey".getBytes(StandardCharsets.UTF_8), 16);
byte[] authKey = HKDF.fromHmacSha256().expand(key, "authKey".getBytes(StandardCharsets.UTF_8), 32);
複製代碼

在咱們解密任何東西以前,咱們將驗證 MAC。首先咱們像以前同樣計算 MAC;不要忘記以前添加的相關數據。

SecretKey macKey = new SecretKeySpec(authKey, "HmacSHA256");
Mac hmac = Mac.getInstance("HmacSHA256");
hmac.init(macKey);
hmac.update(iv);
hmac.update(cipherText);
if (associatedData != null) {
    hmac.update(associatedData);
}
byte[] refMac = hmac.doFinal();
複製代碼

比較 mac 時,咱們須要一個恆定的時間比較函數來避免旁道攻擊閱讀此文了解爲何這很重要。幸運的是咱們可使用 MessageDigest.isEquals()(舊的 bug 已在 Java 6u17 中修復):

if (!MessageDigest.isEqual(refMac, mac)) {
    throw new SecurityException("could not authenticate");
}
複製代碼

做爲最後一步,咱們最終能夠解密咱們的消息。

final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(encKey, "AES"), new IvParameterSpec(iv));
byte[] plainText = cipher.doFinal(cipherText);
複製代碼

以上即是全部內容,若是你想查看一個完整的例子,請查看我託管到 Github 中的一個使用 AES-CBC 的項目 Armadillo。若是你遇到了什麼問題,也能夠在 Gist 中找到這個確切的示例。


總結

咱們演示了使用密碼分組連接(CBC)的 AES 和使用 HMAC 的 Encrypt-then-MAC 架構提供了咱們但願從加密協議中看到的全部理想的安全屬性:保密性、完整性和真實性。

能夠看出,僅僅使用了 GCM,協議就變得複雜了。可是,這些原語一般在全部 Java/Android 環境中均可用,所以它多是你惟一的選擇。請考慮如下事項:

  • 使用 16 字節隨機初始化向量(使用強 PRNG
  • 使用 128 位以上的 MAC 長度(HMAC-SHA256 輸出 256 位)
  • 使用 Encrypt-then-Mac
  • 使用 KDF 派生出 2 個子密鑰
  • 解密以前進行驗證(末日原則
  • 經過使用恆定時間等於實現來防止定時攻擊
  • 使用 128 位加密密鑰長度(你會沒事的!)
  • 將全部內容整合到一條消息中

參考資料:

進一步閱讀:

最佳安全實踐:在 Java 和 Android 中使用 AES 進行對稱加密

相關文章
相關標籤/搜索