在 Java 和 Android 中实现 AES-GCM

本文是Security Best Practices: Symmetric Encryption with AES in Java and Android 实现部分的译文。原文对 aes 加密模式, 原理,以及为什么使用 iv 和 padding 都进行了非常全面的介绍。

在 Java 和 Android 中实现 AES-GCM

所以到最后 AES-GCM 变得很实用。现在的 Java 有所有的我们需要的工具。但是它的加密 API 可能不是最直接的一个。一个细致的开发者也可能不确定 长度 / 大小 / 默认的使用。注意:如果没有说明,则所有内容都适用于 Java 和 Android

在我们的示例中,我们使用随机生成的 128 位密钥。 传递 192 和 256 位长度的密钥时,Java 将自动选择正确的模式。 但请注意,256 位加密通常需要在 JRE 中安装JCE Unlimited Strength Jurisdiction Policy(Android 就很好)。

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

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

byte[] iv = new byte[_12_]; //永远不要使用同一个key。使用同一个key,findbug 也会提示的
secureRandom.nextBytes(iv);

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

final Cipher cipher = Cipher._getInstance_("AES/GCM/NoPadding_"_);
GCMParameterSpec parameterSpec = new GCMParameterSpec(_128_, iv); //128位鉴权标签的长度
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);
//在原文中lv 的length 设为4 byte,但在demo实现中实际为1byte。最好设置为4 byte ,方便客户端解密
byteBuffer.putInt(iv.length);
byteBuffer.put(iv);
byteBuffer.put(cipherText);
byte[] cipherMessage = byteBuffer.array();

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

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

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

Arrays.fill(key,(byte) 0); // 用零重写key的内容

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

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

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 并且可能很快填满你的堆,使得拒绝服务攻击变得微不足道。

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

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

这就是全部了,如果你想看一个完整的例子,请查看我使用 AES-GCM. 的Github 项目 Armadillo