原文地址:Security Best Practices: Symmetric Encryption with AES in Java and Androidhtml
我將在本文中爲你們介紹高級加密標準(AES),常見塊模式,爲何須要填充和初始化向量以及如何保護數據不被篡改。最後,我將爲你們展現如何使用 Java 輕鬆實現此功能,從而避免大多數安全問題。java
AES,又稱 Rijndael 加密算法,在 2000 年被 NIST 選中以用來替換過期的數據加密標準(DES)。AES 是一種分組密碼,這意味着加密發生在固定長度的比特組上。在咱們的例子中,算法定義塊長度爲 128 位。AES 支持 128,192 和 256 位的密鑰長度。android
每一個塊都經歷多輪轉換。我將在這裏省略算法的細節,對算法感興趣的讀者能夠參考維基百科中有關 AES 的文章。這裏須要指出的是塊大小受轉換輪次的重複次數影響(128 位密鑰是 10 個週期,256 位爲 14 個週期),而密鑰長度並不影響它的大小。git
一直到 2009 年 5 月,惟一一次成功發佈,針對完整 AES 的攻擊是對某些特定實現的旁道攻擊。(資源)github
AES 只會加密 128 位數據,若是咱們想要加密整個消息,咱們須要選擇一種塊模式,利用該模式能夠將多個塊加密爲一個密文。最簡單的塊模式是電子密碼本或 ECB。它將在每一個區塊中使用相同的未更改的鍵:算法
這將是特別糟糕的,由於相同的明文會被加密成相同的密文。apache
請記住,除非你只加密小於 128 位的數據,不然永遠不要選擇該模式。不幸的是,它仍然被常常誤用,由於它不須要你提供初始向量(稍後會詳細介紹),所以開發人員彷佛更容易處理。api
必須使用塊模式處理的一種狀況:若是最後一個塊的大小不足 128 位會發生什麼?這就是填充發揮做用的地方,即填充塊的缺失位。最簡單的方式是用零填充缺失位。在 AES 中選擇填充幾乎沒有任何安全隱患。數組
那麼有什麼方案能夠替代 ECB 呢?例如 CBC,在該模式中,用當前的明文塊和前一個密文塊進行異或。在該方法中,每一個密文塊都依賴於它前面的全部明文塊。使用與以前相同的圖片,加密結果將是與噪聲數據沒法區分的隨機數據:緩存
那如何處理第一個塊呢?最簡單的方法是使用一個完整的填充塊(好比用零填充),但這樣每次加密相同密鑰和明文都會產生同樣的密文。此外,若是你爲不一樣的明文重用相同的密鑰,那麼恢復密鑰將會更加容易。更好的方法是使用隨機初始化向量(IV)。這對於隨機數據來講只是一個奇特的詞,大約是一個塊(128 位)大小。將它想象成一個加密的 salt,也就是說,IV 是能夠公開的,隨機的且只能使用一次。但請注意,由於 CBC 將密文異或而不是前一個明文的明文,所以 IV 不只僅會阻止第一個塊的解密。
在傳輸或保持數據時,一般只將 IV 添加到實際的密碼消息中。若是你對如何正確使用 AES-CBC 感興趣,請閱讀本系列的第 2 部分。
另一種選擇是使用 CTR 模式。這種模式頗有意思,由於它會將密碼轉換爲密碼流,這意味着不須要進行填充。在其基本形式中,全部塊的編號爲 0 到 n。如今每一個塊都將使用密鑰、IV(此處也稱爲 nonce)和計數器的值來進行加密。
與 CBC 不一樣,它的優勢是能夠進行並行加密而且全部塊都依賴於 IV,而不只僅是第一個。一個很嚴重的警告是,IV 永遠不能被相同的密鑰重用,由於攻擊者能夠從中輕鬆計算出你所使用的密鑰。
事實:加密不會自動防止數據修改。這其實是一種很是常見的攻擊。有關該問題更全面的討論,請閱讀此文。
那麼咱們又能作些什麼呢?咱們只需將加密驗證碼(MAC)添加到加密郵件中。MAC 相似於數字簽名,不一樣之處在於驗證和驗證密鑰其實是相同的。這種方法有不一樣的變化,大多數研究人員推薦的模式叫作 Encrypt-then-Mac 。也就是說,在加密以後,在密文上計算並附加 MAC。你一般會使用基於哈希的消息身份驗證代碼(HMAC)做爲 MAC 的類型。
如今它開始變得複雜了。爲了完整性/真實性咱們必須選擇 MAC 算法,選擇加密標籤模式,計算 mac 並附加它。由於整個消息必須處理兩次,因此該操做運行速度緩慢。反向操做必須與前面一致,但用於解密和驗證。
若是有模式能夠處理全部的身份驗證,那不是很好嗎?幸運的是有一種稱爲認證加密的加密方式,它同時爲數據的機密性、完整性和真實性提供了保證。支持此功能最流行的塊模式之一爲 Galois/Counter Mode or GCM(好比它可使用 TLS v1.2 中的密碼組件)。
GCM 基於 CTR 模式,它還在加密期間順序計算身份驗證標記。而後該標記一般會附加到密文中。它的大小是一個重要的安全屬性,所以它的長度至少是 128 位。
它還能夠驗證未包括在明文中的附加信息。該數據稱爲關聯數據。這爲何有用呢?例如,加密數據具備元屬性,即用於檢查是否必須從新加載內容的建立日期。攻擊者能夠輕鬆更改建立日期,但若是將其添加爲關聯數據, CGM 將驗證此信息並識別出更改。
直覺會說:越大越好 - 很明顯,強制 256 位隨機值比 128 位更難。根據咱們目前的理解,強制經過 128 位長字節的全部值都須要天文數量的能量,對於任何在合理時間內的人來講都是不現實的(看着你,NSA)。所以,決定基本上在無限和無限時間 2¹²⁸ 之間。
AES 實際上有三種不一樣的密鑰大小,由於它被選爲美國聯邦政府的標註加密算法以用於聯邦政府「包括軍方」控制的各個領域。(...)所以,精明的軍事首腦提出了應該有三個「安全級別」的想法,以便使用重量級方法加密最重要的祕密,但較低價值的數據能夠用更實用,更輕量級的算法加密。(...)所以,NIST 決定正式遵照規定(要求三個關鍵尺寸),但也要作前瞻性的事(最低級別必須經過可碰見的技術不可攻破)(來源)。
論點以下:AES 加密消息可能不會被暴力破壞密鑰破壞,而是經過其餘較便宜的攻擊(當前未知)。這些攻擊對於 128 位密鑰模式和 256 位模式同樣有害,所以在這種狀況下選擇更大的密鑰大小也無濟於事。
因此基本上 128 位密鑰對於大多數用例來講都足夠安全,但量子計算機保護除外。一樣使用比 256 位更快的 128 位加密。128 位密鑰的密鑰強度彷佛能夠更好的防止相關密鑰攻擊(但這與大多數實際用途無關)。
旁道攻擊是利用特定於某些實現的問題的攻擊。加密密碼方案自己不能有效地保護它們。簡單的 AES 實現可能容易發生計時,緩存攻擊及其餘攻擊。
做爲一個很是基本的例子:一個容易發生定時攻擊的簡單算法是一個比較兩個祕密字節數組的 equals()
方法。若是 equals()
有一個快速返回,意味着在第一對不匹配的字節結束循環以後,攻擊者能夠測量 equals()
完成所須要的時間,而且能夠一個字節一個字節的猜想,直到所有匹配爲止。
在這種狀況下,一個修復方法是使用恆定時間等於。請注意,在相似於 JVM 等解釋語言中編寫常量時間代碼每每並不是易事。
針對 AES 的定時和緩存攻擊不只僅是理論上的,甚至能夠經過網絡進行實施。雖然防止旁道攻擊主要是實施加密原語的開發人員關注的問題,但瞭解編碼實踐可能對整個例程的安全性有害是明智的。最通常的主題是,可觀察到的與時間相關的行爲不該該依賴於私密數據。此外,你應該仔細考慮要選擇的實現方案。例如,使用帶有 OpenJDK 的 Java 8+ 和默認的 JCA 提供程序應該在內部使用 Intel 的 AES-NI 指令集,該指令集經過恆定時間和在硬件中實現(同時仍具備良好的性能)來防止大多數時序和緩存攻擊。Android 使用它的 AndroidOpenSSLProvider,內部可能會在硬件中使用 AES(ARM TrustZone),具體取決於 SoC。但我不相信它具備與 Intels pedant 相同的防禦。但即便你改進硬件,也可使用其餘攻擊向量,例如功率分析。存在專門用於防止大多數這些問題的專用硬件,即硬件安全模塊(HSM)。不幸的是,這些設備的成本一般高達數千美圓(有趣的是:你的基於芯片的信用卡也是 HSM)。
最後它變得實用了。如今 Java 擁有咱們須要的全部工具,但加密 API 可能不是最直接的。細心的開發人員也可能不肯定要使用的長度/大小/默認值。注意:若是沒有說明,全部內容都一樣適用於 Java 和 Android。
在咱們的示例中,咱們使用隨機生成的 128 位密鑰。傳遞 192 和 256 位長度的密鑰時,Java 會自動選擇正確的模式。但請注意,256 位加密一般須要在 JRE 中安裝 無政策限制權限文件(Android 中無需安裝)。
SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);
SecretKey secretKey = SecretKeySpec(key, 「AES」);
複製代碼
而後咱們必須建立咱們的初始化向量。對於 CGM,NIST 建議使用 12 字節(非16字節!)隨機字數組,由於它更快,更安全。請注意始終使用像 SecureRandom 這樣的強僞隨機數生成器(RNG)。
byte[] iv = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY
secureRandom.nextBytes(iv);
複製代碼
而後初始化你的密碼。AES-GCM 模式應該適用於大多數現代 JRE 和 Android v2.3 以上版本(雖然僅在 SDK 21+ 上能夠徹底正常運行)。若是碰巧不可用,請安裝像 BouncyCastle 這樣的自定義加密提供程序,但一般首選默認提供程序。咱們選擇 128 位大小的認證標籤。
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, iv); //128 bit auth tag length
cipher.init(Cipher.ENCRYPT_MODE, secretKey, parameterSpec);
複製代碼
若是須要,添加可選的關聯數據(例如元數據)
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
複製代碼
加密;若是你正在加密大塊數據,請研究 CipherInputStream,這樣整個內容就無需加載到堆中。
byte[] cipherText = cipher.doFinal(plainText);
複製代碼
如今將全部內容鏈接到一條消息。
ByteBuffer byteBuffer = ByteBuffer.allocate(4 + iv.length + cipherText.length);
byteBuffer.putInt(iv.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();
複製代碼
若是你須要字符串表示,可選用 Base64 來編碼它。 Android 中有該編碼的標準實現,JDK 僅從版本 8 開始(若是可能,我會避免使用 Apache Commons Codec,由於它很慢且實現混亂)。
這基本上就是加密。爲了構造消息,IV 長度,IV,加密數據和認證標籤被附加到單個字節數組。(在 Java 中,身份驗證標記會自動附加到消息中,沒法使用標準加密 API 自行處理)。
最佳事件是儘量快地從內存中擦除加密密鑰或 IV 等敏感數據。因爲 Java 是一種具備自動內存管理的語言,所以咱們沒法保證如下內容可以預期工做,但在大多數狀況下應該如此:
Arrays.fill(key,(byte) 0); //overwrite the content of key with zeros
複製代碼
注意不要覆蓋仍在其餘地方使用的數據。
如今到解密部分,它的工做原理相似加密,首先解構消息:
ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
int ivLength = byteBuffer.getInt();
if(ivLength < 12 || ivLength >= 16) { // check input parameter
throw new IllegalArgumentException("invalid iv length");
}
byte[] iv = new byte[ivLength];
byteBuffer.get(iv);
byte[] cipherText = new byte[byteBuffer.remaining()];
byteBuffer.get(cipherText);
複製代碼
當心驗證輸入參數,好比 IV 長度,由於攻擊者可能會將長度值更改成如 2³¹,它會分配 2 GiB內存並可能很快填滿你的堆,使得拒絕服務攻擊變得微不足道。
初始化密碼並添加可選的關聯數據並解密:
final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new GCMParameterSpec(128, iv));
if (associatedData != null) {
cipher.updateAAD(associatedData);
}
byte[] plainText= cipher.doFinal(cipherText);
複製代碼
以上即是全部內容,若是你想查看一個完整的例子,請查看我託管到 Github 中的一個使用 AES-GCM 的項目 Armadillo。
咱們須要三個屬性來保護咱們的數據:
具備 Galois/Counter(GCM)塊模式的 AES 提供全部這些屬性,而且至關容易使用,而且在大多數 Java/Android環境中均可用。 請考慮如下事項:
最佳安全實踐:在 Java 和 Android 中使用 AES 進行對稱加密:第2部分:AES-CBC + HMAC。