我了解了更多有關密碼的知識,發現您需要了解更多信息。 我寫的內容在一定程度上是正確的,但是除非您小心謹慎,否則敏感數據可能并非對所有攻擊者都是安全的。
開箱即用Shiro提供了Blowfish-CBC和AES-CBC加密方法,我建議使用它們。 兩者都旨在防止被動竊聽攻擊者并且擅長于此。 不幸的是,真正的攻擊者更加復雜,可能會破壞基于他們的系統。
注意“可能”。 攻擊者只有在被攻擊系統至少與他配合時才能成功。 如果要使用這些密碼,則必須知道如何安全地編寫系統。 當然,另一種選擇是使用更強的密碼并完全避免該問題。
第一章中很少解釋所需的理論術語和概念 。 第二章介紹了如何以更安全的方式加密數據。 然后,我們描述了Blowfish-CBC和AES-CBC的工作方式,并展示了對它們的兩種可能的攻擊 。 最后,該帖子的末尾包含對其他資源的引用
我們從一些理論概念開始。 如果您不想閱讀它,請直接轉到解決方案的章節。
首先要描述的重要事情是被動攻擊者和主動攻擊者之間的區別。 然后,我們解釋什么是分組密碼以及什么是經過身份驗證的加密。 最后兩個子章節列出了選定的易受攻擊的密碼和選定的安全密碼。
僅竊聽攻擊者通常是被動的。 他能夠讀取加密的通信,但無法對其進行修改或將新的密文發送給通信應用程序。 他無法影響交流,他只會聽。 他的被動性只有一個例外:攻擊者能夠將未加密的信息提供給通信方之一,并獲得具有該確切信息的密文。
現實世界中的攻擊者通常更活躍。 他們編寫自己的消息,并將其發送到通信應用程序。 然后,該應用程序假定這些消息已加密,因此它將嘗試對其進行解密。 攻擊者觀察其反應(返回的錯誤代碼,需要的響應時間等),并進一步了解密碼。
如果幸運的話,他可能會使用獲得的知識來破解密碼或植入虛假信息。
分組密碼只能加密短消息。 例如,AES只能加密16個字節長的消息,而Blowfish只能加密8個字節長的消息。
較長的消息分為多個塊。 每個塊與先前加密的塊組合,然后傳遞到塊密碼。 塊組合被稱為操作模式,并且有多種安全方式來實現。
本文討論的兩種主動攻擊是對CBC操作模式的攻擊。 分組密碼本身是安全的。
經過身份驗證的加密將任何修改后的密文視為無效。 無法獲取加密的數據,對其進行修改并以有效的密文結尾。 此屬性也稱為密文完整性。
密碼首先檢查完整性,然后以相同方式拒絕所有已修改的消息。 由于攻擊者無法通過完整性檢查,因此他從向應用程序發送新消息中得不到任何好處。
身份驗證和密文完整性通常(但不總是)由操作模式提供。
任何不提供密文完整性或未經過身份驗證的加密的密碼都可能會受到一些主動攻擊。 使用哪個加密庫都沒有關系。 加密算法是在標準中定義的,并且與所使用的庫無關,其含義相同。
換句話說,如果可以在不知道秘密密鑰的情況下創建有效的密文,那么即使不知道,也可能存在一些主動攻擊。
這篇文章描述了兩種攻擊CBC的操作模式。 一旦了解了這些攻擊以及被動和主動攻擊者之間的區別,您就應該能夠對CFB,CTR,OFB或其他未經身份驗證的密碼模式提出類似的攻擊。
經過身份驗證的加密可以防止主動攻擊者的攻擊。 還提供身份驗證的最常見操作模式是:
- GCM
- EAX
- CCM
用這些模式之一(例如AES-EAX)替換CBC,您將擁有一個針對活動攻擊者的安全密碼。
當然,針對主動攻擊者的安全并不意味著該密碼沒有其他實際限制。 例如,大多數密碼只有在用戶加密了太多數據后才更改密鑰時才是安全的。 如果您認真對待數據加密,則應該研究并了解這些限制。
本章介紹如何在Java中使用經過身份驗證的加密。 對于那些不了解該理論的人 ,認證加密可以防止數據篡改。 沒有密鑰的任何人都不能修改加密的消息,并且不可能對這種密碼進行主動攻擊。
Apache Shiro沒有實現自己的加密算法。 它將所有工作委托給Java密碼學擴展(JCE),每個Java運行時中都可用。 Shiro“僅”提供易于使用的API和安全的默認值。
因此,我們必須做兩件事:
- 將經過身份驗證的操作模式安裝到Java密碼擴展中,
- 將Shiro與新的經過驗證的操作模式集成。
Java密碼術擴展是一個可擴展的API。 所有類和算法都是由提供程序創建的,新提供程序可以隨時安裝到系統中。 提供者可以是:
- 安裝到Java運行時中,并且可用于所有Java應用程序,
- 與應用程序一起分發和初始化。
我們將展示如何將Bouncy Castle添加到項目中。 Bouncy Castle是根據MIT許可分發的提供商,并且包含EAX和GCM操作模式。
Bouncy Castle分發了多個jar文件,每個文件針對不同的Java版本進行了優化。 由于我們的演示項目使用Java 6,因此我們必須使用bcprov-jdk16
庫。 將maven依賴項添加到pom.xml中:
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk16</artifactId><version>1.46</version>
</dependency>
庫存在后,您必須將其提供程序安裝到java安全系統中。 在應用程序啟動時運行以下方法:
private BouncyCastleProvider installBouncyCastle() {BouncyCastleProvider provider = new BouncyCastleProvider();Security.addProvider(provider);return provider;
}
現在已安裝Bouncy Castle,并且兩種驗證操作模式都可用。
Shiro加密軟件包基本上是JCE上易于使用的外觀。 它將JCE對象配置為使用安全默認值,并為其添加線程安全性。
擴展DefaultBlockCipherService
類以利用這些功能。 大多數配置都由該類完成,但是我們仍然必須指定三件事:
- 分組密碼名稱和參數,
- 操作模式,
- 填充。
塊密碼名稱在構造函數參數中指定。 我們將使用AES,因為它不需要其他配置。 GCM和CCM都不需要填充,因此我們必須指定填充NONE
。
新密碼服務:
class GCMCipherService extends DefaultBlockCipherService {private static final String ALGORITHM_NAME = "AES";public GCMCipherService() {super(ALGORITHM_NAME);setMode(OperationMode.GCM);setPaddingScheme(PaddingScheme.NONE);}}
這就對了。 您可以將新的身份驗證密碼用作任何其他Shiro密碼服務 。
我們創建了一個簡單的測試用例,以證明完整性檢查有效。 它加密消息并更改其密文的第三個字節。 如果密碼提供了身份驗證,則嘗試解密修改后的密文將導致運行時異常:
@Test
public void testGCMAuthentication() {String message = "secret message";GCMCipherService gcmCipher = new GCMCipherService();assertIngetrityCheck(message, gcmCipher);
}private void assertIngetrityCheck(String message, DefaultBlockCipherService cipher) {byte[] key = cipher.generateNewKey().getEncoded();byte[] messageBytes = CodecSupport.toBytes(message);ByteSource encrypt = cipher.encrypt(messageBytes, key);// change the ciphertextencrypt.getBytes()[3] = 0;try {// it should be impossible to decrypt changed ciphertextcipher.decrypt(encrypt.getBytes(), key).getBytes();} catch (Exception ex) {return;}fail("It should not be possible to decrypt changed ciphertext.");
}
根據文檔 ,Java 7支持兩種經過身份驗證的操作模式:CCM和GCM。 從理論上講,您不需要第三方加密提供程序。
不幸的是,Oracle無法在第一個JDK 7版本中提供這些模式的完整實現。 他們希望將其添加到更新版本中,因此情況將來可能會發生變化。 Oracle錯誤數據庫包含兩個相關的 錯誤 。
Java 1.7.0_01仍然沒有它們。
在描述承諾的攻擊之前,我們需要做的最后一件事是密碼塊鏈接(CBC)操作模式。 這種操作模式足以抵御被動攻擊者,相當快速且易于實施。
不幸的是,它也容易受到主動攻擊。
CBC用于使用分組密碼對長消息進行加密。 分組密碼只能加密短數據塊,因此它首先將消息拆分為短數據塊。
第一個和最后一個塊是特殊情況。 我們將在以下子章節中說明如何處理它們。 現在,假設消息開頭已被加密,并且其第i個塊m i
與密文c i
相對應。
分兩個步驟對下一個消息塊進行加密:
- 用上一個塊的密文對該塊進行異或(例如,
m i ?c i-1
), - 使用分組密碼(例如
Blowfish(key, m i ?c i-1 )
)對結果進行加密。
示例:假設該秘密消息有三個塊,我們正在嘗試使用Blowfish-CBC對其進行加密。 第一塊已經被加密,其密文為1, 2, 3, 4, 5, 6, 7, 8
。 第二個塊是字節數組1, 0, 1, 0, 1, 0, 1, 0
。
步驟1:將第一個塊密文與第二個塊異或:
1, 2, 3, 4, 5, 6, 7, 8 ? 1, 0, 1, 0, 1, 0, 1, 0 = 0, 2, 2, 4, 4, 6, 6, 8
步驟2:使用河豚算法對結果進行加密:
Blowfish(secret_key, {0, 2, 2, 4, 4, 6, 6, 8})
第一個塊沒有要合并的前一個塊。 因此,我們將生成一個稱為初始化向量的隨機數據塊。 初始化向量用作數據的第一塊。 它與第一個消息塊進行異或運算,并使用塊密碼對結果進行加密。
初始化向量作為密文的第一個塊未經加密地發送。 如果沒有密文,接收者將無法解密密文,也沒有理由對其保密。
示例:假設該秘密消息有三個塊,我們正在嘗試使用Blowfish-CBC對其進行加密。 第一個塊是一個字節數組1, 1, 1, 1, 1, 1, 1, 1
。
步驟1:產生隨機初始化向量:
1, 8, 2, 7, 3, 6, 4, 5
步驟2:將第一個塊與初始化向量進行異或:
1, 8, 2, 7, 3, 6, 4, 5 ? 1, 1, 1, 1, 1, 1, 1, 1 = 0, 9, 3, 6, 2, 7, 5, 4
步驟3:使用河豚算法對結果進行加密:
Blowfish(secret_key, {0, 9, 3, 6, 2, 7, 5, 4})
步驟4:結合初始化向量和密文。 如果上一步中的Blowfish函數結果為1, 2, 3, 4, 5, 6, 7, 8
,則密文為:
1, 8, 2, 7, 3, 6, 4, 5, 1, 2, 3, 4, 5, 6, 7, 8
分組密碼能夠加密固定長度的消息,而最后一個分組通常比該分組短。 由于密碼無法加密,因此我們需要一種在消息末尾添加其他字節的方法。
Shiro默認使用PKCS#5填充。 每個字節等于填充的長度:
- 如果最后一塊太短,請計算丟失的字節數,并用該數字填充丟失的字節。
- 如果最后一個塊的大小正確,則將其視為丟失整個塊的消息。 向其添加一個新的填充塊。 每個字節都等于塊大小。
示例1:假設秘密消息有三個塊,我們正在嘗試使用Blowfish-CBC對其進行加密。 最后的塊是字節數組1, 1, 1, 1
。 填充塊:
1, 1, 1, 1, 4, 4, 4, 4
示例2:假設該秘密消息有三個塊,我們正在嘗試使用Blowfish-CBC對其進行加密。 最后一個塊是字節數組8, 7, 6, 5, 4, 3, 2, 1
。 最后一塊和填充:
8, 7, 6, 5, 4, 3, 2, 1, 8, 8, 8, 8, 8, 8, 8, 8
最后,我們準備展示對基于CBC的密碼的兩種不同攻擊,并證明問題是真實的。 我們的樣本項目包含針對AES-CBC和Blowfish-CBC密碼的兩種攻擊的測試案例。
第一部分說明如何將加密文本的開頭更改為我們選擇的任何消息。 第二小節介紹了如何解密密文。
僅當攻擊者已經知道加密消息的內容時 ,才可能進行數據篡改攻擊。 這樣的攻擊者可以將第一個消息塊更改為他希望的任何內容。
特別是可以:
- 更改AES-CBC加密消息的前16個字節,
- 更改Blowfish-CBC加密郵件的前8個字節。
這種攻擊是否危險,在很大程度上取決于情況。
如果您使用密碼通過網絡發送密碼,那么數據篡改并不是那么危險。 最壞的情況是,合法用戶的登錄將被拒絕。 同樣,如果加密的數據存儲在某些只讀存儲中,則不必擔心數據被篡改。
但是,如果要通過網絡發送銀行訂單,則數據篡改是真正的威脅。 如果有人將消息Pay Mark 100$
更改為Pay Tom 9999$
,Tom將獲得9999 $,而他不應該得到。
加密的消息分為三個部分:
- 隨機初始向量
- 第一組密文,
- 消息的其余部分。
接收者使用塊密文解密第一個密文塊。 這給他message block?initial vector
。 要獲取消息,他必須將此值與初始向量進行異或運算:
message block ? initial vector ? initial vector = message block
初始向量與消息一起傳輸,主動攻擊者可以更改它。 如果攻擊者用another iv
替換了原始初始向量,則接收者將解密另一條消息:
message block ? initial vector ? another iv = another message
重新排列前面的等式,您將得到:
another iv = message block ? initial vector ? another message
如果攻擊者同時知道加密消息的初始向量和內容,則可以將消息修改為所需的任何內容。 他要做的就是將已知消息內容和所需消息都與原始初始向量異或。
解密修改后的密文的任何人都將獲得修改后的消息,而不是原始消息。
我們創建了一個簡單的測試案例,演示了對使用AES-CBC加密的消息的這種攻擊。
想象一下,攻擊者捕獲了一封加密的電子郵件,并且以某種方式知道其中的內容:
//original message
private static final String EMAIL = "Hi,\n" +"send Martin all requested money please.\n\n" +"With Regards, \n" +"Accounting\n";
攻擊者只能更改消息的前16個字節,因此他決定將資金重定向到Andrea:
//changed message
private static final String MODIFICATION = "Hi,\n" +"give Andrea all requested money please.\n\n" +"With Regards, \n" +"Accounting\n";
以下測試用例對消息進行加密并修改密文。 修改后的密文被解密并與預期消息進行比較:
@Test
public void testModifiedMessage_AES() {//create cipher and the secret key StringCipherService cipher = new StringCipherService(new AesCipherService());byte[] key = cipher.generateNewKey();//encrypt the messagebyte[] ciphertext = cipher.encrypt(EMAIL, key);//attack: modify the encrypted messagefor (int i = 0; i < 16; i++) {ciphertext[i] = (byte)(ciphertext[i] ^ MODIFICATION.getBytes()[i] ^ EMAIL.getBytes()[i]);}//decrypt and verifyString result = cipher.decrypt(ciphertext, key);assertEquals(MODIFICATION, result);
}
當然,可以對河豚CBC進行類似的攻擊。 這次我們只能更改前8個字節:
@Test
public void testModifiedMessage_Blowfish() {String email = "Pay 100 dollars to them, but nothing more. Accounting\n";StringCipherService cipher = new StringCipherService(new BlowfishCipherService());byte[] key = cipher.generateNewKey();byte[] ciphertext = cipher.encrypt(email, key);String modified = "Pay 900 dollars to them, but nothing more. Accounting\n";for (int i = 0; i < 8; i++) {ciphertext[i] = (byte)(ciphertext[i] ^ modified.getBytes()[i] ^ email.getBytes()[i]);}String result = cipher.decrypt(ciphertext, key);assertEquals(modified, result);
}
第二次攻擊使攻擊者可以解密該秘密消息。 僅當解密機密消息的應用程序與攻擊者合作時,才有可能進行攻擊。
攻擊者會創建許多偽密文,并將其發送給收件人。 當他嘗試解密這些消息時,將發生以下情況之一:
- 密文解密為毫無意義的垃圾,
- 修改后的消息將根本不是有效的密文。
如果應用程序在兩種情況下的行為均相同,則一切正常。 如果行為不同,則攻擊者能夠解密密文。 基于CBC的密文只有一種錯誤的方法-如果填充錯誤。
例如,如果密文包含加密的密碼,則當解密的密碼錯誤時,易受攻擊的服務器可能會以“拒絕登錄”進行響應,而在密文無效的情況下,則可能會出現運行時異常。 在這種情況下,攻擊者可以恢復密碼。
每條虛假消息都有兩個部分:一個虛假的初始向量和一個消息塊。 兩者都發送到服務器。 如果它回答“正確填充”,那么我們知道:
message ? original iv ? fake iv = valid padding
上述方程式中唯一未知的變量是消息。 original iv
是先前的密文塊, fake iv
是由我們創建的,有效填充是1
或2, 2
或3, 3, 3
或...
或8, 8, ..., 8
等之一。
因此,我們可以將塊內容計算為:
message = valid padding ? original iv ? fake iv
從恢復最后一個塊字節開始。 每個偽初始向量都以0開頭,以不同的最后字節結尾。 這樣,我們幾乎可以確定服務器僅在以1結尾的消息上回答“向右填充”。使用上一章公式計算最后一個塊字節。
獲取消息的倒數第二個字節非常相似。 唯一的區別是我們必須制作一個密文,該密文解密為第二個最短的填充2, 2
。 消息的最后一個字節是已知的,因此將2強制為最后一個值很容易。 初始向量的開頭并不重要,請將其設置為0。
然后,我們嘗試初始向量的倒數第二個字節的所有可能值。 服務器回答“正確填充”后,我們可以從與前面相同的公式中獲取倒數第二個消息字節: original iv ? fake iv ? 2
original iv ? fake iv ? 2
original iv ? fake iv ? 2
。
我們計算倒數第三個消息字節出來的假消息與填充3, 3, 3
; 用填充4、4、4、4填充消息中的第四4, 4, 4, 4
; 以此類推,直到整個塊被解密為止。
易受攻擊的服務器使用PaddingOraculum
類進行模擬。 此類的每個實例都會生成一個隨機密鑰,并將其保密。 它僅公開兩個公共方法:
-
byte[] encrypt(String message)
–用密鑰加密一個字符串, -
boolean verifyPadding(byte[] ciphertext)
–返回填充是否正確。
填充oraculum攻擊是用decryptLastBlock
方法實現的。 該方法解密加密消息的最后一塊:
private String decryptLastBlock(PaddingOraculum oraculum, byte[] ciphertext) {// extract relevant part of the ciphertextbyte[] ivAndBlock = getLastTwoBlocks(ciphertext, oraculum.getBlockSize());// modified initial vectorbyte[] ivMod = new byte[oraculum.getBlockSize()];Arrays.fill(ivMod, (byte) 0);// Start with last byte of the last block and // continue to the first byte.for (int i = oraculum.getBlockSize()-1; i >= 0; i--) {// add padding to the initial vector int expectedPadding = oraculum.getBlockSize() - i;xorPad(ivMod, expectedPadding);// loop through possible values of ivModification[i]for (ivMod[i] = -128; ivMod[i] < 127; ivMod[i]++) {// create fake message and verify its paddingbyte[] modifiedCiphertext = replaceBeginning(ivAndBlock, ivMod);if (oraculum.verifyPadding(modifiedCiphertext)) {// we can stop looping// the ivModification[i] =// = solution ^ expectedPadding ^ ivAndBlock[i]break;}}// remove the padding from the initial vectorxorPad(ivMod, expectedPadding);}// modified initial vector now contains the solution xor // original initial vectorString result = "";for (int i = 0; i < ivMod.length; i++) {ivMod[i] = (byte) (ivMod[i] ^ ivAndBlock[i]);result += (char) ivMod[i];}return result;
}
我們的示例項目包含兩個測試用例 。 一個人用AES-CBC加密消息,然后使用填充字眼到密文的最后一塊。 另一個對河豚-CBC也做同樣的事情。
解密Blowfish-CBC測試案例:
@Test
public void testPaddingOracle_Blowfish() {String message = "secret message!";PaddingOraculum oraculum = new PaddingOraculum(new BlowfishCipherService());//Oraculum encrypts the message with a secret key.byte[] ciphertext = oraculum.encrypt(message);//use oraculum to decrypt the messageString result = decryptLastBlock(oraculum, ciphertext);//the original message had padding 1assertEquals("essage!"+(char)1, result);
}
其他相關資源:
- 免費在線斯坦福加密課程 。
- 從2002年開始用padding oracle攻擊的原始文件。
- 填充oracle攻擊的一個很好的替代解釋 。
- 解釋了對CBC的野獸襲擊 。 它基于padding oracle攻擊。
- 關于常見密碼錯誤的堆棧交換線程 。
沒有密碼可以絕對安全地防御所有可能的攻擊。 相反,它們僅提供針對定義明確的攻擊類別的保護。 只有對系統的潛在威脅與密碼強度匹配時,密碼才是安全的。
可以通過兩種方式來防御主動攻擊:
- 通過設計使主動攻擊不可能。
- 使用經過身份驗證的加密。
使用經過身份驗證的加密可以說更容易,并且應該是首選。 排除主動攻擊者容易出錯,而且風險更大。
Github上提供了本文中使用的所有代碼示例。
參考: This is Stuff博客上的JCG合作伙伴 Maria Jurcovicova提供的Java安全加密 。
翻譯自: https://www.javacodegeeks.com/2012/05/secure-encryption-in-java.html