[译] 最佳安全实践:在 Java 和 Android 中使用 AES 进行对称加密
原文地址:Security Best Practices: Symmetric Encryption with AES in Java and Android
最佳安全實(shí)踐:在 Java 和 Android 中使用 AES 進(jìn)行對(duì)稱(chēng)加密
我將在本文中為大家介紹高級(jí)加密標(biāo)準(zhǔn)(AES),常見(jiàn)塊模式,為什么需要填充和初始化向量以及如何保護(hù)數(shù)據(jù)不被篡改。最后,我將為大家展示如何使用 Java 輕松實(shí)現(xiàn)此功能,從而避免大多數(shù)安全問(wèn)題。
為什么每一個(gè)軟件工程師都需要知道 AES
AES,又稱(chēng) Rijndael 加密算法,在 2000 年被 NIST 選中以用來(lái)替換過(guò)時(shí)的數(shù)據(jù)加密標(biāo)準(zhǔn)(DES)。AES 是一種分組密碼,這意味著加密發(fā)生在固定長(zhǎng)度的比特組上。在我們的例子中,算法定義塊長(zhǎng)度為 128 位。AES 支持 128,192 和 256 位的密鑰長(zhǎng)度。
每個(gè)塊都經(jīng)歷多輪轉(zhuǎn)換。我將在這里省略算法的細(xì)節(jié),對(duì)算法感興趣的讀者可以參考維基百科中有關(guān) AES 的文章。這里需要指出的是塊大小受轉(zhuǎn)換輪次的重復(fù)次數(shù)影響(128 位密鑰是 10 個(gè)周期,256 位為 14 個(gè)周期),而密鑰長(zhǎng)度并不影響它的大小。
一直到 2009 年 5 月,唯一一次成功發(fā)布,針對(duì)完整 AES 的攻擊是對(duì)某些特定實(shí)現(xiàn)的旁道攻擊。(資源)
想要加密多個(gè)塊?
AES 只會(huì)加密 128 位數(shù)據(jù),如果我們想要加密整個(gè)消息,我們需要選擇一種塊模式,利用該模式可以將多個(gè)塊加密為一個(gè)密文。最簡(jiǎn)單的塊模式是電子密碼本或 ECB。它將在每個(gè)區(qū)塊中使用相同的未更改的鍵:
圖片來(lái)自維基百科這將是特別糟糕的,因?yàn)橄嗤拿魑臅?huì)被加密成相同的密文。
使用 ECB 塊模式加密的圖片顯示原始圖案(自己嘗試一下)請(qǐng)記住,除非你只加密小于 128 位的數(shù)據(jù),否則永遠(yuǎn)不要選擇該模式。不幸的是,它仍然被經(jīng)常誤用,因?yàn)樗恍枰闾峁┏跏枷蛄?#xff08;稍后會(huì)詳細(xì)介紹),因此開(kāi)發(fā)人員似乎更容易處理。
必須使用塊模式處理的一種情況:如果最后一個(gè)塊的大小不足 128 位會(huì)發(fā)生什么?這就是填充發(fā)揮作用的地方,即填充塊的缺失位。最簡(jiǎn)單的方式是用零填充缺失位。在 AES 中選擇填充幾乎沒(méi)有任何安全隱患。
密碼分組鏈接(CBC)
那么有什么方案可以替代 ECB 呢?例如 CBC,在該模式中,用當(dāng)前的明文塊和前一個(gè)密文塊進(jìn)行異或。在該方法中,每個(gè)密文塊都依賴(lài)于它前面的所有明文塊。使用與之前相同的圖片,加密結(jié)果將是與噪聲數(shù)據(jù)無(wú)法區(qū)分的隨機(jī)數(shù)據(jù):
使用 CBC 塊模式加密的圖片看起來(lái)是隨機(jī)的那如何處理第一個(gè)塊呢?最簡(jiǎn)單的方法是使用一個(gè)完整的填充塊(比如用零填充),但這樣每次加密相同密鑰和明文都會(huì)產(chǎn)生一樣的密文。此外,如果你為不同的明文重用相同的密鑰,那么恢復(fù)密鑰將會(huì)更加容易。更好的方法是使用隨機(jī)初始化向量(IV)。這對(duì)于隨機(jī)數(shù)據(jù)來(lái)說(shuō)只是一個(gè)奇特的詞,大約是一個(gè)塊(128 位)大小。將它想象成一個(gè)加密的 salt,也就是說(shuō),IV 是可以公開(kāi)的,隨機(jī)的且只能使用一次。但請(qǐng)注意,因?yàn)?CBC 將密文異或而不是前一個(gè)明文的明文,因此 IV 不僅僅會(huì)阻止第一個(gè)塊的解密。
在傳輸或保持?jǐn)?shù)據(jù)時(shí),通常只將 IV 添加到實(shí)際的密碼消息中。如果你對(duì)如何正確使用 AES-CBC 感興趣,請(qǐng)閱讀本系列的第 2 部分。
記數(shù)模式(CTR)
另外一種選擇是使用 CTR 模式。這種模式很有意思,因?yàn)樗鼤?huì)將密碼轉(zhuǎn)換為密碼流,這意味著不需要進(jìn)行填充。在其基本形式中,所有塊的編號(hào)為 0 到 n。現(xiàn)在每個(gè)塊都將使用密鑰、IV(此處也稱(chēng)為 nonce)和計(jì)數(shù)器的值來(lái)進(jìn)行加密。
圖片來(lái)自維基百科與 CBC 不同,它的優(yōu)點(diǎn)是可以進(jìn)行并行加密并且所有塊都依賴(lài)于 IV,而不僅僅是第一個(gè)。一個(gè)很?chē)?yán)重的警告是,IV 永遠(yuǎn)不能被相同的密鑰重用,因?yàn)楣粽呖梢詮闹休p松計(jì)算出你所使用的密鑰。
我可以確保沒(méi)有人能夠修改我的消息嗎?
事實(shí):加密不會(huì)自動(dòng)防止數(shù)據(jù)修改。這實(shí)際上是一種非常常見(jiàn)的攻擊。有關(guān)該問(wèn)題更全面的討論,請(qǐng)閱讀此文。
那么我們又能做些什么呢?我們只需將加密驗(yàn)證碼(MAC)添加到加密郵件中。MAC 類(lèi)似于數(shù)字簽名,不同之處在于驗(yàn)證和驗(yàn)證密鑰實(shí)際上是相同的。這種方法有不同的變化,大多數(shù)研究人員推薦的模式叫做 Encrypt-then-Mac 。也就是說(shuō),在加密之后,在密文上計(jì)算并附加 MAC。你通常會(huì)使用基于哈希的消息身份驗(yàn)證代碼(HMAC)作為 MAC 的類(lèi)型。
現(xiàn)在它開(kāi)始變得復(fù)雜了。為了完整性/真實(shí)性我們必須選擇 MAC 算法,選擇加密標(biāo)簽?zāi)J?#xff0c;計(jì)算 mac 并附加它。因?yàn)檎麄€(gè)消息必須處理兩次,所以該操作運(yùn)行速度緩慢。反向操作必須與前面一致,但用于解密和驗(yàn)證。
使用 GCM 進(jìn)行認(rèn)證加密
如果有模式可以處理所有的身份驗(yàn)證,那不是很好嗎?幸運(yùn)的是有一種稱(chēng)為認(rèn)證加密的加密方式,它同時(shí)為數(shù)據(jù)的機(jī)密性、完整性和真實(shí)性提供了保證。支持此功能最流行的塊模式之一為 Galois/Counter Mode or GCM(比如它可以使用 TLS v1.2 中的密碼組件)。
GCM 基于 CTR 模式,它還在加密期間順序計(jì)算身份驗(yàn)證標(biāo)記。然后該標(biāo)記通常會(huì)附加到密文中。它的大小是一個(gè)重要的安全屬性,因此它的長(zhǎng)度至少是 128 位。
它還可以驗(yàn)證未包括在明文中的附加信息。該數(shù)據(jù)稱(chēng)為關(guān)聯(lián)數(shù)據(jù)。這為什么有用呢?例如,加密數(shù)據(jù)具有元屬性,即用于檢查是否必須重新加載內(nèi)容的創(chuàng)建日期。攻擊者可以輕松更改創(chuàng)建日期,但如果將其添加為關(guān)聯(lián)數(shù)據(jù), CGM 將驗(yàn)證此信息并識(shí)別出更改。
激烈的討論:使用多長(zhǎng)的密鑰?
直覺(jué)會(huì)說(shuō):越大越好 - 很明顯,強(qiáng)制 256 位隨機(jī)值比 128 位更難。根據(jù)我們目前的理解,強(qiáng)制通過(guò) 128 位長(zhǎng)字節(jié)的所有值都需要天文數(shù)量的能量,對(duì)于任何在合理時(shí)間內(nèi)的人來(lái)說(shuō)都是不現(xiàn)實(shí)的(看著你,NSA)。因此,決定基本上在無(wú)限和無(wú)限時(shí)間 212? 之間。
AES 實(shí)際上有三種不同的密鑰大小,因?yàn)樗贿x為美國(guó)聯(lián)邦政府的標(biāo)注加密算法以用于聯(lián)邦政府「包括軍方」控制的各個(gè)領(lǐng)域。(...)因此,精明的軍事首腦提出了應(yīng)該有三個(gè)“安全級(jí)別”的想法,以便使用重量級(jí)方法加密最重要的秘密,但較低價(jià)值的數(shù)據(jù)可以用更實(shí)用,更輕量級(jí)的算法加密。(...)因此,NIST 決定正式遵守規(guī)定(要求三個(gè)關(guān)鍵尺寸),但也要做前瞻性的事(最低級(jí)別必須通過(guò)可遇見(jiàn)的技術(shù)不可攻破)(來(lái)源)。
論點(diǎn)如下:AES 加密消息可能不會(huì)被暴力破壞密鑰破壞,而是通過(guò)其他較便宜的攻擊(當(dāng)前未知)。這些攻擊對(duì)于 128 位密鑰模式和 256 位模式一樣有害,因此在這種情況下選擇更大的密鑰大小也無(wú)濟(jì)于事。
所以基本上 128 位密鑰對(duì)于大多數(shù)用例來(lái)說(shuō)都足夠安全,但量子計(jì)算機(jī)保護(hù)除外。同樣使用比 256 位更快的 128 位加密。128 位密鑰的密鑰強(qiáng)度似乎可以更好的防止相關(guān)密鑰攻擊(但這與大多數(shù)實(shí)際用途無(wú)關(guān))。
旁注:旁道攻擊
旁道攻擊是利用特定于某些實(shí)現(xiàn)的問(wèn)題的攻擊。加密密碼方案本身不能有效地保護(hù)它們。簡(jiǎn)單的 AES 實(shí)現(xiàn)可能容易發(fā)生計(jì)時(shí),緩存攻擊及其他攻擊。
作為一個(gè)非常基本的例子:一個(gè)容易發(fā)生定時(shí)攻擊的簡(jiǎn)單算法是一個(gè)比較兩個(gè)秘密字節(jié)數(shù)組的 equals() 方法。如果 equals() 有一個(gè)快速返回,意味著在第一對(duì)不匹配的字節(jié)結(jié)束循環(huán)之后,攻擊者可以測(cè)量 equals() 完成所需要的時(shí)間,并且可以一個(gè)字節(jié)一個(gè)字節(jié)的猜測(cè),直到全部匹配為止。
使用快速返回可能受到定時(shí)攻擊的代碼在這種情況下,一個(gè)修復(fù)方法是使用恒定時(shí)間等于。請(qǐng)注意,在類(lèi)似于 JVM 等解釋語(yǔ)言中編寫(xiě)常量時(shí)間代碼往往并非易事。
針對(duì) AES 的定時(shí)和緩存攻擊不僅僅是理論上的,甚至可以通過(guò)網(wǎng)絡(luò)進(jìn)行實(shí)施。雖然防止旁道攻擊主要是實(shí)施加密原語(yǔ)的開(kāi)發(fā)人員關(guān)注的問(wèn)題,但了解編碼實(shí)踐可能對(duì)整個(gè)例程的安全性有害是明智的。最一般的主題是,可觀察到的與時(shí)間相關(guān)的行為不應(yīng)該依賴(lài)于私密數(shù)據(jù)。此外,你應(yīng)該仔細(xì)考慮要選擇的實(shí)現(xiàn)方案。例如,使用帶有 OpenJDK 的 Java 8+ 和默認(rèn)的 JCA 提供程序應(yīng)該在內(nèi)部使用 Intel 的 AES-NI 指令集,該指令集通過(guò)恒定時(shí)間和在硬件中實(shí)現(xiàn)(同時(shí)仍具有良好的性能)來(lái)防止大多數(shù)時(shí)序和緩存攻擊。Android 使用它的 AndroidOpenSSLProvider,內(nèi)部可能會(huì)在硬件中使用 AES(ARM TrustZone),具體取決于 SoC。但我不相信它具有與 Intels pedant 相同的防護(hù)。但即使你改進(jìn)硬件,也可以使用其他攻擊向量,例如功率分析。存在專(zhuān)門(mén)用于防止大多數(shù)這些問(wèn)題的專(zhuān)用硬件,即硬件安全模塊(HSM)。不幸的是,這些設(shè)備的成本通常高達(dá)數(shù)千美元(有趣的是:你的基于芯片的信用卡也是 HSM)。
在 Java 和 Android 中實(shí)現(xiàn) AES-GCM
最后它變得實(shí)用了。現(xiàn)在 Java 擁有我們需要的所有工具,但加密 API 可能不是最直接的。細(xì)心的開(kāi)發(fā)人員也可能不確定要使用的長(zhǎng)度/大小/默認(rèn)值。注意:如果沒(méi)有說(shuō)明,所有內(nèi)容都同樣適用于 Java 和 Android。
在我們的示例中,我們使用隨機(jī)生成的 128 位密鑰。傳遞 192 和 256 位長(zhǎng)度的密鑰時(shí),Java 會(huì)自動(dòng)選擇正確的模式。但請(qǐng)注意,256 位加密通常需要在 JRE 中安裝 無(wú)政策限制權(quán)限文件(Android 中無(wú)需安裝)。
SecureRandom secureRandom = new SecureRandom(); byte[] key = new byte[16]; secureRandom.nextBytes(key); SecretKey secretKey = SecretKeySpec(key, “AES”); 復(fù)制代碼然后我們必須創(chuàng)建我們的初始化向量。對(duì)于 CGM,NIST 建議使用 12 字節(jié)(非16字節(jié)!)隨機(jī)字?jǐn)?shù)組,因?yàn)樗?#xff0c;更安全。請(qǐng)注意始終使用像 SecureRandom 這樣的強(qiáng)偽隨機(jī)數(shù)生成器(RNG)。
byte[] iv = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY secureRandom.nextBytes(iv); 復(fù)制代碼然后初始化你的密碼。AES-GCM 模式應(yīng)該適用于大多數(shù)現(xiàn)代 JRE 和 Android v2.3 以上版本(雖然僅在 SDK 21+ 上可以完全正常運(yùn)行)。如果碰巧不可用,請(qǐng)安裝像 BouncyCastle 這樣的自定義加密提供程序,但通常首選默認(rèn)提供程序。我們選擇 128 位大小的認(rèn)證標(biāo)簽。
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); 復(fù)制代碼如果需要,添加可選的關(guān)聯(lián)數(shù)據(jù)(例如元數(shù)據(jù))
if (associatedData != null) {cipher.updateAAD(associatedData); } 復(fù)制代碼加密;如果你正在加密大塊數(shù)據(jù),請(qǐng)研究 CipherInputStream,這樣整個(gè)內(nèi)容就無(wú)需加載到堆中。
byte[] cipherText = cipher.doFinal(plainText); 復(fù)制代碼現(xiàn)在將所有內(nèi)容連接到一條消息。
ByteBuffer byteBuffer = ByteBuffer.allocate(4 + iv.length + cipherText.length); byteBuffer.putInt(iv.length); byteBuffer.put(iv); byteBuffer.put(cipherText); byte[] cipherMessage = byteBuffer.array(); 復(fù)制代碼如果你需要字符串表示,可選用 Base64 來(lái)編碼它。 Android 中有該編碼的標(biāo)準(zhǔn)實(shí)現(xiàn),JDK 僅從版本 8 開(kāi)始(如果可能,我會(huì)避免使用 Apache Commons Codec,因?yàn)樗苈覍?shí)現(xiàn)混亂)。
這基本上就是加密。為了構(gòu)造消息,IV 長(zhǎng)度,IV,加密數(shù)據(jù)和認(rèn)證標(biāo)簽被附加到單個(gè)字節(jié)數(shù)組。(在 Java 中,身份驗(yàn)證標(biāo)記會(huì)自動(dòng)附加到消息中,無(wú)法使用標(biāo)準(zhǔn)加密 API 自行處理)。
最佳事件是盡可能快地從內(nèi)存中擦除加密密鑰或 IV 等敏感數(shù)據(jù)。由于 Java 是一種具有自動(dòng)內(nèi)存管理的語(yǔ)言,因此我們無(wú)法保證以下內(nèi)容能夠預(yù)期工作,但在大多數(shù)情況下應(yīng)該如此:
Arrays.fill(key,(byte) 0); //overwrite the content of key with zeros 復(fù)制代碼注意不要覆蓋仍在其他地方使用的數(shù)據(jù)。
現(xiàn)在到解密部分,它的工作原理類(lèi)似加密,首先解構(gòu)消息:
ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage); int ivLength = byteBuffer.getInt(); if(ivLength < 12 || ivLength >= 16) { // check input parameterthrow new IllegalArgumentException("invalid iv length"); } byte[] iv = new byte[ivLength]; byteBuffer.get(iv); byte[] cipherText = new byte[byteBuffer.remaining()]; byteBuffer.get(cipherText); 復(fù)制代碼小心驗(yàn)證輸入?yún)?shù),比如 IV 長(zhǎng)度,因?yàn)楣粽呖赡軙?huì)將長(zhǎng)度值更改為如 231,它會(huì)分配 2 GiB內(nèi)存并可能很快填滿(mǎn)你的堆,使得拒絕服務(wù)攻擊變得微不足道。
初始化密碼并添加可選的關(guān)聯(lián)數(shù)據(jù)并解密:
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); 復(fù)制代碼以上便是所有內(nèi)容,如果你想查看一個(gè)完整的例子,請(qǐng)查看我托管到 Github 中的一個(gè)使用 AES-GCM 的項(xiàng)目 Armadillo。
總結(jié)
我們需要三個(gè)屬性來(lái)保護(hù)我們的數(shù)據(jù):
- 保密性:防止竊聽(tīng)者發(fā)現(xiàn)明文消息或有關(guān)明文消息的信息的能力。
- 完整性:防止攻擊者在合法用戶(hù)未注意的情況下修改消息的能力。
- 真實(shí)性:證明消息是由特定方生成并防止偽造新消息的能力。 這通常通過(guò)消息驗(yàn)證代碼(MAC)提供。注意,真實(shí)性也意味著完整性。
具有 Galois/Counter(GCM)塊模式的 AES 提供所有這些屬性,并且相當(dāng)容易使用,并且在大多數(shù) Java/Android環(huán)境中都可用。 請(qǐng)考慮以下事項(xiàng):
- 使用永遠(yuǎn)不會(huì)與相同密鑰一起使用的 12 字節(jié)初始化向量(使用像 SecureRandom 這樣的強(qiáng)偽隨機(jī)數(shù)生成器)。
- 使用 128 位身份驗(yàn)證標(biāo)記長(zhǎng)度。
- 使用 128 位密鑰長(zhǎng)度(你會(huì)沒(méi)事的!)。
- 將所有內(nèi)容整合到一條消息中。
進(jìn)一步閱讀:
最佳安全實(shí)踐:在 Java 和 Android 中使用 AES 進(jìn)行對(duì)稱(chēng)加密:第2部分:AES-CBC + HMAC。
參考資料:
- patrickfav/armadillo
- patrickfav/bytes-java
總結(jié)
以上是生活随笔為你收集整理的[译] 最佳安全实践:在 Java 和 Android 中使用 AES 进行对称加密的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 2018年,JavaScript都经历了
- 下一篇: 一张图看懂阿里云网络产品【十五】IPv6