您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Model Center   Code  
会员   
   
 
     
   
 订阅
  捐助
最佳安全实践:在 Java 和 Android 中使用 AES 进行对称加密
 
  1388  次浏览      16
 2018-12-28
 
编辑推荐:
本文来自于网络, 本文中为大家介绍高级加密标准(AES),常见块模式,为什么需要填充和初始化向量以及如何保护数据不被篡改.

为什么每一个软件工程师都需要知道 AES

AES,又称 Rijndael 加密算法,在 2000 年被 NIST 选中以用来替换过时的数据加密标准(DES)。AES 是一种分组密码,这意味着加密发生在固定长度的比特组上。在我们的例子中,算法定义块长度为 128 位。AES 支持 128,192 和 256 位的密钥长度。

每个块都经历多轮转换。我将在这里省略算法的细节,对算法感兴趣的读者可以参考维基百科中有关 AES 的文章。这里需要指出的是块大小受转换轮次的重复次数影响(128 位密钥是 10 个周期,256 位为 14 个周期),而密钥长度并不影响它的大小。

一直到 2009 年 5 月,唯一一次成功发布,针对完整 AES 的攻击是对某些特定实现的旁道攻击。(资源)

想要加密多个块?

AES 只会加密 128 位数据,如果我们想要加密整个消息,我们需要选择一种块模式,利用该模式可以将多个块加密为一个密文。最简单的块模式是电子密码本或 ECB。它将在每个区块中使用相同的未更改的键:

图片来自维基百科

这将是特别糟糕的,因为相同的明文会被加密成相同的密文。

使用 ECB 块模式加密的图片显示原始图案(自己尝试一下点击预览)

请记住,除非你只加密小于 128 位的数据,否则永远不要选择该模式。不幸的是,它仍然被经常误用,因为它不需要你提供初始向量(稍后会详细介绍),因此开发人员似乎更容易处理。

必须使用块模式处理的一种情况:如果最后一个块的大小不足 128 位会发生什么?这就是填充发挥作用的地方,即填充块的缺失位。最简单的方式是用零填充缺失位。在 AES 中选择填充几乎没有任何安全隐患。

密码分组链接(CBC)

那么有什么方案可以替代 ECB 呢?例如 CBC,在该模式中,用当前的明文块和前一个密文块进行异或。在该方法中,每个密文块都依赖于它前面的所有明文块。使用与之前相同的图片,加密结果将是与噪声数据无法区分的随机数据:

使用 CBC 块模式加密的图片看起来是随机的

那如何处理第一个块呢?最简单的方法是使用一个完整的填充块(比如用零填充),但这样每次加密相同密钥和明文都会产生一样的密文。此外,如果你为不同的明文重用相同的密钥,那么恢复密钥将会更加容易。更好的方法是使用随机初始化向量(IV)。这对于随机数据来说只是一个奇特的词,大约是一个块(128 位)大小。将它想象成一个加密的 salt,也就是说,IV 是可以公开的,随机的且只能使用一次。但请注意,因为 CBC 将密文异或而不是前一个明文的明文,因此 IV 不仅仅会阻止第一个块的解密。

在传输或保持数据时,通常只将 IV 添加到实际的密码消息中。如果你对如何正确使用 AES-CBC 感兴趣,请阅读本系列的第 2 部分。

记数模式(CTR)

另外一种选择是使用 CTR 模式。这种模式很有意思,因为它会将密码转换为密码流,这意味着不需要进行填充。在其基本形式中,所有块的编号为 0 到 n。现在每个块都将使用密钥、IV(此处也称为 nonce)和计数器的值来进行加密。

图片来自维基百科

与 CBC 不同,它的优点是可以进行并行加密并且所有块都依赖于 IV,而不仅仅是第一个。一个很严重的警告是,IV 永远不能被相同的密钥重用,因为攻击者可以从中轻松计算出你所使用的密钥。

我可以确保没有人能够修改我的消息吗?

事实:加密不会自动防止数据修改。这实际上是一种非常常见的攻击。有关该问题更全面的讨论,请阅读此文。

那么我们又能做些什么呢?我们只需将加密验证码(MAC)添加到加密邮件中。MAC 类似于数字签名,不同之处在于验证和验证密钥实际上是相同的。这种方法有不同的变化,大多数研究人员推荐的模式叫做 Encrypt-then-Mac 。也就是说,在加密之后,在密文上计算并附加 MAC。你通常会使用基于哈希的消息身份验证代码(HMAC)作为 MAC 的类型。

现在它开始变得复杂了。为了完整性/真实性我们必须选择 MAC 算法,选择加密标签模式,计算 mac 并附加它。因为整个消息必须处理两次,所以该操作运行速度缓慢。反向操作必须与前面一致,但用于解密和验证。

使用 GCM 进行认证加密

如果有模式可以处理所有的身份验证,那不是很好吗?幸运的是有一种称为认证加密的加密方式,它同时为数据的机密性、完整性和真实性提供了保证。支持此功能最流行的块模式之一为 Galois/Counter Mode or GCM(比如它可以使用 TLS v1.2 中的密码组件)。

GCM 基于 CTR 模式,它还在加密期间顺序计算身份验证标记。然后该标记通常会附加到密文中。它的大小是一个重要的安全属性,因此它的长度至少是 128 位。

它还可以验证未包括在明文中的附加信息。该数据称为关联数据。这为什么有用呢?例如,加密数据具有元属性,即用于检查是否必须重新加载内容的创建日期。攻击者可以轻松更改创建日期,但如果将其添加为关联数据, CGM 将验证此信息并识别出更改。

激烈的讨论:使用多长的密钥?

直觉会说:越大越好 - 很明显,强制 256 位随机值比 128 位更难。根据我们目前的理解,强制通过 128 位长字节的所有值都需要天文数量的能量,对于任何在合理时间内的人来说都是不现实的(看着你,NSA)。因此,决定基本上在无限和无限时间 212? 之间。

AES 实际上有三种不同的密钥大小,因为它被选为美国联邦政府的标注加密算法以用于联邦政府「包括军方」控制的各个领域。(...)因此,精明的军事首脑提出了应该有三个“安全级别”的想法,以便使用重量级方法加密最重要的秘密,但较低价值的数据可以用更实用,更轻量级的算法加密。(...)因此,NIST 决定正式遵守规定(要求三个关键尺寸),但也要做前瞻性的事(最低级别必须通过可遇见的技术不可攻破)(来源)。

论点如下:AES 加密消息可能不会被暴力破坏密钥破坏,而是通过其他较便宜的攻击(当前未知)。这些攻击对于 128 位密钥模式和 256 位模式一样有害,因此在这种情况下选择更大的密钥大小也无济于事。

所以基本上 128 位密钥对于大多数用例来说都足够安全,但量子计算机保护除外。同样使用比 256 位更快的 128 位加密。128 位密钥的密钥强度似乎可以更好的防止相关密钥攻击(但这与大多数实际用途无关)。

旁注:旁道攻击

旁道攻击是利用特定于某些实现的问题的攻击。加密密码方案本身不能有效地保护它们。简单的 AES 实现可能容易发生计时,缓存攻击及其他攻击。

作为一个非常基本的例子:一个容易发生定时攻击的简单算法是一个比较两个秘密字节数组的 equals() 方法。如果 equals() 有一个快速返回,意味着在第一对不匹配的字节结束循环之后,攻击者可以测量 equals() 完成所需要的时间,并且可以一个字节一个字节的猜测,直到全部匹配为止。

使用快速返回可能受到定时攻击的代码

在这种情况下,一个修复方法是使用恒定时间等于。请注意,在类似于 JVM 等解释语言中编写常量时间代码往往并非易事。

针对 AES 的定时和缓存攻击不仅仅是理论上的,甚至可以通过网络进行实施。虽然防止旁道攻击主要是实施加密原语的开发人员关注的问题,但了解编码实践可能对整个例程的安全性有害是明智的。最一般的主题是,可观察到的与时间相关的行为不应该依赖于私密数据。此外,你应该仔细考虑要选择的实现方案。例如,使用带有 OpenJDK 的 Java 8+ 和默认的 JCA 提供程序应该在内部使用 Intel 的 AES-NI 指令集,该指令集通过恒定时间和在硬件中实现(同时仍具有良好的性能)来防止大多数时序和缓存攻击。Android 使用它的 AndroidOpenSSLProvider,内部可能会在硬件中使用 AES(ARM TrustZone),具体取决于 SoC。但我不相信它具有与 Intels pedant 相同的防护。但即使你改进硬件,也可以使用其他攻击向量,例如功率分析。存在专门用于防止大多数这些问题的专用硬件,即硬件安全模块(HSM)。不幸的是,这些设备的成本通常高达数千美元(有趣的是:你的基于芯片的信用卡也是 HSM)。

在 Java 和 Android 中实现 AES-GCM

最后它变得实用了。现在 Java 拥有我们需要的所有工具,但加密 API 可能不是最直接的。细心的开发人员也可能不确定要使用的长度/大小/默认值。注意:如果没有说明,所有内容都同样适用于 Java 和 Android。

在我们的示例中,我们使用随机生成的 128 位密钥。传递 192 和 256 位长度的密钥时,Java 会自动选择正确的模式。但请注意,256 位加密通常需要在 JRE 中安装 无政策限制权限文件(Android中是好的)。

SecureRandom secureRandom = new SecureRandom();
byte[] key = new byte[16];
secureRandom.nextBytes(key);
SecretKey secretKey = SecretKeySpec(key, “AES”);

然后我们必须创建我们的初始化向量。对于 CGM,NIST 建议使用 12 字节(非16字节!)随机字数组,因为它更快,更安全。请注意始终使用像 SecureRandom 这样的强伪随机数生成器(RNG)。

byte[] iv = new byte[12]; //NEVER REUSE THIS IV WITH SAME KEY
secureRandom.nextBytes(iv);

然后初始化你的密码。AES-GCM 模式应该适用于大多数现代 JRE 和 Android v2.3 以上版本(虽然仅在 SDK 21+ 上可以完全正常运行)。如果碰巧不可用,请安装像 BouncyCastle 这样的自定义加密提供程序,但通常首选默认提供程序。我们选择 128 位大小的认证标签。

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);

如果需要,添加可选的关联数据(例如元数据)

if (associatedData != null) {
cipher.updateAAD(associatedData);
}

加密;如果你正在加密大块数据,请研究 CipherInputStream,这样整个内容就无需加载到堆中。

byte[] cipherText = cipher.doFinal(plainText);

现在将所有内容连接到一条消息。

ByteBuffer byteBuffer = ByteBuffer.allocate(4 + iv.length + cipherText.length);
byteBuffer.putInt(iv.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();

如果你需要字符串表示,可选用 Base64 来编码它。 Android 中有该编码的标准实现,JDK 仅从版本 8 开始(如果可能,我会避免使用 Apache Commons Codec,因为它很慢且实现混乱)。

这基本上就是加密。为了构造消息,IV 长度,IV,加密数据和认证标签被附加到单个字节数组。(在 Java 中,身份验证标记会自动附加到消息中,无法使用标准加密 API 自行处理)。

最佳事件是尽可能快地从内存中擦除加密密钥或 IV 等敏感数据。由于 Java 是一种具有自动内存管理的语言,因此我们无法保证以下内容能够预期工作,但在大多数情况下应该如此:

Arrays.fill(key,(byte) 0); //overwrite the content of key with zeros

注意不要覆盖仍在其他地方使用的数据。

现在到解密部分,它的工作原理类似加密,首先解构消息:

ByteBuffer byteBuffer = ByteBuffer.wrap(cipherMessage);
int ivLength = byteBuffer.getInt();
if(ivLength < 12 || ivLength >= 16) { // check input parameter
throw new IllegalArgumentException("invalid iv length");
}
byte[] iv = new byte[ivLength];
byteBuffer.get(iv);
byte[] cipherText = new byte[byteBuffer.remaining()];
byteBuffer.get(cipherText);

小心验证输入参数,比如 IV 长度,因为攻击者可能会将长度值更改为如 231,它会分配 2 GiB内存并可能很快填满你的堆,使得拒绝服务攻击变得微不足道。

初始化密码并添加可选的关联数据并解密:

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);

以上便是所有内容,如果你想查看一个完整的例子,请查看我托管到 Github 中的一个使用 AES-GCM 的项目 Armadillo。

总结

我们需要三个属性来保护我们的数据:

保密性:防止窃听者发现明文消息或有关明文消息的信息的能力。

完整性:防止攻击者在合法用户未注意的情况下修改消息的能力。

真实性:证明消息是由特定方生成并防止伪造新消息的能力。 这通常通过消息验证代码(MAC)提供。注意,真实性也意味着完整性。

具有 Galois/Counter(GCM)块模式的 AES 提供所有这些属性,并且相当容易使用,并且在大多数 Java/Android环境中都可用。 请考虑以下事项:

1.使用永远不会与相同密钥一起使用的 12 字节初始化向量(使用像 SecureRandom 这样的强伪随机数生成器)。

2.使用 128 位身份验证标记长度。

3.使用 128 位密钥长度(你会没事的!)。

4.将所有内容整合到一条消息中。

 

   
1388 次浏览       16
 
相关文章

iOS应用安全开发,你不知道的那些事术
Web安全之SQL注入攻击
移动APP安全在渗透测试中的应用
从Google备份互联网看“数据安全”
 
相关文档

web安全设计与防护
互联网海量内容安全处理技术
黑客攻击与防范技术
WEB黑盒安全检测
 
相关课程

WEB网站与应用安全原理与实践
web应用安全架构设计
创建安全的J2EE Web应用代码
信息安全问题与防范