博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
最佳安全实践:在 Java 和 Android 中使用 AES 进行对称加密:第2部分:AES-CBC + HMAC...
阅读量:6904 次
发布时间:2019-06-27

本文共 5878 字,大约阅读时间需要 19 分钟。

本文是我上一篇文章:“” 的续篇,在这篇文章中我总结了关于 AES 最为重要的事情并演示了如何通过 AES-GCM 来使用它。在阅读本文并深入下一个主题之前,我强烈建议你阅读它,因为它解释了最重要的基础知识。


本文讨论了以下可能发生的情况:你不能通过类似 的认证加密模式来使用?你当前使用的平台不支持它,或者你必须兼容老版本或其它第三方协议?,你都不应该放弃它所具有的安全属性:

  • 保密性:没有密钥的人无法阅读该消息
  • 完整性:没有人会修改消息内容
  • 真实性:可以对消息的发送者进行验证

选择非认证加密,比如块模式,不幸的是,由于具备很好的,它缺少后两个安全属性。如何解决这个问题?正如我在上一篇文章中所说的那样,一种可能的解决方案是将加密原语组合在一起以包含。

加密验证码(MAC)

那么什么是 MAC,我们为什么要使用它呢?MAC 类似于散列函数,这意味着它将消息作为输入并生成一个所谓的简短标记。为了确保并非任何人都可以为任意消息创建标记,MAC 函数需要一个密钥来进行计算。与使用非对称加密的签名相比,MAC 可使用相同的密钥来进行标记生成和认证。

例如,如果双方安全地交换了 MAC 密钥,并且每条消息都附加了认证标记,那么它们都可以检查消息是否是由另一方创建的,并且在传输过程中没有被更改。攻击者需要保密的 MAC 密钥来伪造身份进行标记验证。

最广泛使用的 MAC 类型之一是 ,它包含一个,该函数通常是 SHA256。由于我不会详细介绍其算法,因此我建议你阅读相关 。当然还有如 等其他可用于对称加密的类型。几乎所有的加密框架都至少包含一个 HMAC 实现,包括通过 实现的 。

使用加密的 MAC:架构

那么正确应用 MAC 的方法是什么呢?根据安全研究院 的说法,这里有:

  • MAC-then-Encrypt:基于明文生成 MAC,然后将其追加到明文中后再进行加密(在 中使用)
  • Encrypt-then-MAC:基于密文和初始向量生成 MAC,然后将其追加到密文中(在 中使用)
  • Encrypt-and-MAC: 基于明文生成 MAC、然后将其追加到密文中(在 中使用)

每一个选项都有它自己的属性,我建议你通过来获取每个选项的完整参数。总而言之, 使用 。由于 MAC 可以防止不正确消息的解密,它可以防止选择密文攻击。此外也由于 MAC 在密文中运行,它不能泄漏有关明文的任何信息。然而它的缺点是,因为 IV 和标记中必须包含可能的协议/算法版本或类型,因此实施起来稍微有些困难。重要的是在验证 MAC 之前永远不要进行任何加密操作,否则你可能受到 ( 称之为)。

Encrypt-then-Mac 架构

Encrypt-then-Mac 架构

附录:CGM 和 Encrypt-then-MAC 通常情况下它们的安全强度可能类似,CGM 有以下优点:

  • 简单易用而不易出错
  • 更快,因为它只需要一次通过整个信息

它的缺点是只能允许 96 位初始向量(对于 128 位),HMAC 理论上比 GCM 的内部 MAC 算法 GHASH(128 位标记大小对 256 位及以上)更强。GCM 无法进行 。相关详细讨论,请。

使用加密的 MAC:验证密钥

我们必须解决的最后一个问题是:我们应该从哪里获得用于 MAC 计算的密钥?如果使用的是强密钥(即足够随机且可以安全地切换),那么使用与加密相同的密钥(当使用 HMAC 时)似乎。但最佳实践是使用密钥派生函数(KDF)派生出 2 个子密钥以防范未来可能发现的任何问题。这可以像计算主密钥上的 SHA256 并将其拆分为两个 16 字节块一样简单。 但是我更喜欢标准化的协议,比如,它直接支持此场景而不需要字节调整。

2 个子密钥的派生

2 个子密钥的派生


在 Java 和 Android 中使用 EtM 实现 AES-CBC

理论已经足够了,现在让我们开始编码!在接下来的例子中,我将使用 AES-CBC,这是一个看似保守的决定。这样做的原因是,应该 和 版本都可以使用它。如前所述,我们将使用带有 HMAC 的 Encrypt-then-Mac 方案。这里唯一的外部依赖是 。这段代码基本上是我在中描述的 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 这样的。

byte[] iv = new byte[16];secureRandom.nextBytes(iv);

重用 IV 不像 GCM 那样具有灾难性,但最好还是避免使用。在这里可以看到。

下一步,我们将派生出加密和身份验证所需的 2 个子密钥。我们将在配置 HMAC-SHA256()中使用 ,由于它使用起来简单直接。我们使用 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 字节块大小的信息。由于对使用的填充方案似乎,我们选择了支持最广泛的:。

注意: 由于,我们必须将密码套件设置为 PKCS5。除了被定义为了不同的块尺寸,两者几乎完全相同;通常情况下 PKCS#5 与 AES 并不兼容,但由于定义可追溯到使用了 8 字节块的 3DES,我们坚持使用它。如果你的 JCE 提供程序接受 AES/CBC/PKCS7Padding,那么使用此定义更好,如此你的代码将更容易被理解。

final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); //actually uses PKCS#7cipher.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 和加密数据附加到单个字节数组。

如果你需要字符串表示,可以选用 对其进行编码。,JDK 仅从 开始支持(如果可能,我将避免使用 ,因为它很慢且实现混乱)。

由于 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 时,我们需要一个恒定的时间比较函数来避免;。幸运的是我们可以使用 (旧的 bug 已在 中修复):

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 中的一个使用 的项目 。如果你遇到了什么问题,也可以在 中找到这个确切的示例。


总结

我们演示了使用密码分组链接(CBC)的 AES 和使用 HMAC 的 Encrypt-then-MAC 架构提供了我们希望从加密协议中看到的所有理想的安全属性:保密性、完整性和真实性。

可以看出,仅仅使用了 GCM,协议就变得复杂了。但是,这些原语通常在所有 Java/Android 环境中都可用,因此它可能是你唯一的选择。请考虑以下事项:

  • 使用 16 字节随机初始化向量(使用强 )
  • 使用 128 位以上的 MAC 长度(HMAC-SHA256 输出 256 位)
  • 使用 Encrypt-then-Mac
  • 使用 KDF 派生出 2 个子密钥
  • 解密之前进行验证()
  • 通过使用恒定时间等于实现来防止定时攻击
  • 使用 128 位加密密钥长度(你会没事的!)
  • 将所有内容整合到一条消息中

参考资料:

进一步阅读:

转载地址:http://vkmdl.baihongyu.com/

你可能感兴趣的文章
shell学习笔记三
查看>>
我的友情链接
查看>>
纯CSS超过宽度显示省略号
查看>>
17款jQuery在线QQ客服代码分享
查看>>
ipsec在企业网中的应用
查看>>
iptables笔记
查看>>
Kali Linux 教程 之 Kali Linux 更新源
查看>>
xshell远程qemu-kvm虚拟机安装
查看>>
×××配置实例_06:分支机构到中心站点动态IPSEC ×××配置
查看>>
利用开源的驰骋工作流程引擎,生产类企业应用案例之三
查看>>
虚拟机Hyper-v目前急需解决的问题
查看>>
REDIS HGETALL按序输出结果
查看>>
heartbeat安装配置
查看>>
如果我是项目经理:那么这样子。。。
查看>>
CentOS7挂载windows共享时提示写保护
查看>>
我的友情链接
查看>>
git clone出现SSL错误
查看>>
解决mysql-socket报错问题
查看>>
CentOS 5/6.X 使用 EPEL YUM源
查看>>
golang redis驱动的比较
查看>>