我了解了更多有关密码的知识,发现您需要了解更多信息。 我写的内容在一定程度上是正确的,但是除非您小心谨慎,否则敏感数据可能并非对所有攻击者都是安全的。
开箱即用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