摘要:如何解決這個問題正如我在上一篇文章中所說的那樣,一種可能的解決方案是將加密原語組合在一起以包含加密驗證碼。但是,這些原語通常在所有環境中都可用,因此它可能是你唯一的選擇。
本文是我上一篇文章:“最佳安全實踐:在 Java和 Android 中使用 AES 進行對稱加密” 的續篇,在這篇文章中我總結了關于 AES 最為重要的事情并演示了如何通過 AES-GCM 來使用它。在閱讀本文并深入下一個主題之前,我強烈建議你閱讀它,因為它解釋了最重要的基礎知識。
本文討論了以下可能發生的情況:你不能通過類似 Galois/Counter Mode (GCM) 的認證加密模式來使用高級加密標準(AES)?你當前使用的平臺不支持它,或者你必須兼容老版本或其它第三方協議?無論你放棄 GCM 的原因是什么,你都不應該放棄它所具有的安全屬性:
保密性:沒有密鑰的人無法閱讀該消息
完整性:沒有人會修改消息內容
真實性:可以對消息的發送者進行驗證
選擇非認證加密,比如塊模式密碼分組鏈接(CBC),不幸的是,由于具備很好的延展性,它缺少后兩個安全屬性。如何解決這個問題?正如我在上一篇文章中所說的那樣,一種可能的解決方案是將加密原語組合在一起以包含加密驗證碼(MAC)。
加密驗證碼(MAC)那么什么是 MAC,我們為什么要使用它呢?MAC 類似于散列函數,這意味著它將消息作為輸入并生成一個所謂的簡短標記。為了確保并非任何人都可以為任意消息創建標記,MAC 函數需要一個密鑰來進行計算。與使用非對稱加密的簽名相比,MAC 可使用相同的密鑰來進行標記生成和認證。
例如,如果雙方安全地交換了 MAC 密鑰,并且每條消息都附加了認證標記,那么它們都可以檢查消息是否是由另一方創建的,并且在傳輸過程中沒有被更改。攻擊者需要保密的 MAC 密鑰來偽造身份進行標記驗證。
最廣泛使用的 MAC 類型之一是 散列消息密鑰驗證碼(HMAC),它包含一個哈希加密函數,該函數通常是 SHA256。由于我不會詳細介紹其算法,因此我建議你閱讀相關 RFC。當然還有如 CBC-MAC 等其他可用于對稱加密的類型。幾乎所有的加密框架都至少包含一個 HMAC 實現,包括通過 Mac 實現的 JCA/JCE。
使用加密的 MAC:架構那么正確應用 MAC 的方法是什么呢?根據安全研究院 Hugo Krawcyzk 的說法,這里有三種基本選項:
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 稱之為末日原則)。
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 個子密鑰的派生
理論已經足夠了,現在讓我們開始編碼!在接下來的例子中,我將使用 AES-CBC,這是一個看似保守的決定。這樣做的原因是,應該保證幾乎每個 JRE 和 Android 版本都可以使用它。如前所述,我們將使用帶有 HMAC 的 Encrypt-then-Mac 方案。這里唯一的外部依賴是 HKDF。這段代碼基本上是我在上一篇文章中描述的 GCM 示例的一個映射。
加密簡單起見,我們使用隨機生成的 128 位密鑰。當你傳遞 128、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 位加密密鑰長度(你會沒事的!)
將所有內容整合到一條消息中
參考資料:patrickfav/hkdf
patrickfav/armadillo
進一步閱讀:最佳安全實踐:在 Java 和 Android 中使用 AES 進行對稱加密
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/72972.html
摘要:還有很多開發者沒有意識到的加密算法的問題。不要使用哈希函數做為對稱加密算法的簽名。開發者建議使用基于口令的加密算法時,生成密鑰時要加鹽,鹽的取值最好來自,并指定迭代次數。不要使用沒有消息認證的加密算法加密消息,無法防重放。 本文作者:阿里移動安全@伊樵,@舟海 Android開發中,難免會遇到需要加解密一些數據內容存到本地文件、或者通過網絡傳輸到其他服務器和設備的問題,但并不是使用了加...
摘要:所謂對稱加密,就是加密和解密使用同一秘鑰,這也是這種加密算法最顯著的缺點之一。非對稱加密算法由于對稱加密在通信加密領域的缺陷,年和提出了非對稱加密的概念。非對稱加密,其主要缺點之一就是慢,適合加密少量數據。 1. 加密的目的 加密不同于密碼,加密是一個動作或者過程,其目的就是將一段明文信息(人類或機器可以直接讀懂的信息)變為一段看上去沒有任何意義的字符,必須通過事先約定的解密規則才能將...
閱讀 1608·2021-11-23 09:51
閱讀 1178·2019-08-30 13:57
閱讀 2257·2019-08-29 13:12
閱讀 2011·2019-08-26 13:57
閱讀 1193·2019-08-26 11:32
閱讀 978·2019-08-23 15:08
閱讀 699·2019-08-23 14:42
閱讀 3080·2019-08-23 11:41