内容简介:原文地址:本文是我上一篇文章:“本文讨论了以下可能发生的情况:你不能通过类似
原文地址: Security Best Practices: Symmetric Encryption with AES in Java and Android: Part 2: AES-CBC + HMAC
本文是我上一篇文章:“ 最佳安全实践:在 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 称之为末日原则)。
附录: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 密钥派生函数 ,它直接支持此场景而不需要字节调整。
在 Java 和 Android 中使用 EtM 实现 AES-CBC
理论已经足够了,现在让我们开始编码!在接下来的例子中,我将使用 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 位加密密钥长度(你会没事的!)
- 将所有内容整合到一条消息中
以上所述就是小编给大家介绍的《[译]最佳安全实践:在 Java 和 Android 中使用 AES 进行对称加密:第2部分:AES-CBC + HMAC》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- MSSQL实践-数据库备份加密
- [译] 最佳安全实践:在 Java 和 Android 中使用 AES 进行对称加密
- 加密原理详解:对称式加密 VS 非对称式加密
- 编码、摘要和加密(三)——数据加密
- 聊聊对称加密与非对称加密
- 手机游戏加密之2d资源加密
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。