原文地址:Security Best Practices: Symmetric Encryption with AES in Java and Androidhtml
我将在本文中为你们介绍高级加密标准(AES),常见块模式,为何须要填充和初始化向量以及如何保护数据不被篡改。最后,我将为你们展现如何使用 Java 轻松实现此功能,从而避免大多数安全问题。java
AES,又称 Rijndael 加密算法,在 2000 年被 NIST 选中以用来替换过期的数据加密标准(DES)。AES 是一种分组密码,这意味着加密发生在固定长度的比特组上。在咱们的例子中,算法定义块长度为 128 位。AES 支持 128,192 和 256 位的密钥长度。android
每一个块都经历多轮转换。我将在这里省略算法的细节,对算法感兴趣的读者能够参考维基百科中有关 AES 的文章。这里须要指出的是块大小受转换轮次的重复次数影响(128 位密钥是 10 个周期,256 位为 14 个周期),而密钥长度并不影响它的大小。git
一直到 2009 年 5 月,惟一一次成功发布,针对完整 AES 的攻击是对某些特定实现的旁道攻击。(资源)github
AES 只会加密 128 位数据,若是咱们想要加密整个消息,咱们须要选择一种块模式,利用该模式能够将多个块加密为一个密文。最简单的块模式是电子密码本或 ECB。它将在每一个区块中使用相同的未更改的键:算法
这将是特别糟糕的,由于相同的明文会被加密成相同的密文。apache
请记住,除非你只加密小于 128 位的数据,不然永远不要选择该模式。不幸的是,它仍然被常常误用,由于它不须要你提供初始向量(稍后会详细介绍),所以开发人员彷佛更容易处理。api
必须使用块模式处理的一种状况:若是最后一个块的大小不足 128 位会发生什么?这就是填充发挥做用的地方,即填充块的缺失位。最简单的方式是用零填充缺失位。在 AES 中选择填充几乎没有任何安全隐患。数组
那么有什么方案能够替代 ECB 呢?例如 CBC,在该模式中,用当前的明文块和前一个密文块进行异或。在该方法中,每一个密文块都依赖于它前面的全部明文块。使用与以前相同的图片,加密结果将是与噪声数据没法区分的随机数据:缓存
那如何处理第一个块呢?最简单的方法是使用一个完整的填充块(好比用零填充),但这样每次加密相同密钥和明文都会产生同样的密文。此外,若是你为不一样的明文重用相同的密钥,那么恢复密钥将会更加容易。更好的方法是使用随机初始化向量(IV)。这对于随机数据来讲只是一个奇特的词,大约是一个块(128 位)大小。将它想象成一个加密的 salt,也就是说,IV 是能够公开的,随机的且只能使用一次。但请注意,由于 CBC 将密文异或而不是前一个明文的明文,所以 IV 不只仅会阻止第一个块的解密。
在传输或保持数据时,一般只将 IV 添加到实际的密码消息中。若是你对如何正确使用 AES-CBC 感兴趣,请阅读本系列的第 2 部分。
另一种选择是使用 CTR 模式。这种模式颇有意思,由于它会将密码转换为密码流,这意味着不须要进行填充。在其基本形式中,全部块的编号为 0 到 n。如今每一个块都将使用密钥、IV(此处也称为 nonce)和计数器的值来进行加密。
与 CBC 不一样,它的优势是能够进行并行加密而且全部块都依赖于 IV,而不只仅是第一个。一个很严重的警告是,IV 永远不能被相同的密钥重用,由于攻击者能够从中轻松计算出你所使用的密钥。
事实:加密不会自动防止数据修改。这其实是一种很是常见的攻击。有关该问题更全面的讨论,请阅读此文。
那么咱们又能作些什么呢?咱们只需将加密验证码(MAC)添加到加密邮件中。MAC 相似于数字签名,不一样之处在于验证和验证密钥其实是相同的。这种方法有不一样的变化,大多数研究人员推荐的模式叫作 Encrypt-then-Mac 。也就是说,在加密以后,在密文上计算并附加 MAC。你一般会使用基于哈希的消息身份验证代码(HMAC)做为 MAC 的类型。
如今它开始变得复杂了。为了完整性/真实性咱们必须选择 MAC 算法,选择加密标签模式,计算 mac 并附加它。由于整个消息必须处理两次,因此该操做运行速度缓慢。反向操做必须与前面一致,但用于解密和验证。
若是有模式能够处理全部的身份验证,那不是很好吗?幸运的是有一种称为认证加密的加密方式,它同时为数据的机密性、完整性和真实性提供了保证。支持此功能最流行的块模式之一为 Galois/Counter Mode or GCM(好比它可使用 TLS v1.2 中的密码组件)。
GCM 基于 CTR 模式,它还在加密期间顺序计算身份验证标记。而后该标记一般会附加到密文中。它的大小是一个重要的安全属性,所以它的长度至少是 128 位。
它还能够验证未包括在明文中的附加信息。该数据称为关联数据。这为何有用呢?例如,加密数据具备元属性,即用于检查是否必须从新加载内容的建立日期。攻击者能够轻松更改建立日期,但若是将其添加为关联数据, CGM 将验证此信息并识别出更改。
直觉会说:越大越好 - 很明显,强制 256 位随机值比 128 位更难。根据咱们目前的理解,强制经过 128 位长字节的全部值都须要天文数量的能量,对于任何在合理时间内的人来讲都是不现实的(看着你,NSA)。所以,决定基本上在无限和无限时间 2¹²⁸ 之间。
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 拥有咱们须要的全部工具,但加密 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 长度,由于攻击者可能会将长度值更改成如 2³¹,它会分配 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。
咱们须要三个属性来保护咱们的数据:
具备 Galois/Counter(GCM)块模式的 AES 提供全部这些属性,而且至关容易使用,而且在大多数 Java/Android环境中均可用。 请考虑如下事项:
最佳安全实践:在 Java 和 Android 中使用 AES 进行对称加密:第2部分:AES-CBC + HMAC。