【问题标题】:Java Cipher.update does not write to buffer when using AES/GCM (Android 9)使用 AES/GCM (Android 9) 时,Java Cipher.update 不会写入缓冲区
【发布时间】:2019-12-14 08:57:02
【问题描述】:

我正在尝试在 Android 上使用 javax.crypto.Cipher 来使用 AES-GCM 加密数据流。据我了解,可以多次使用 Cipher.update 进行多部分加密操作,并使用 Cipher.doFinal 完成。但是,当使用 AES/GCM/NoPadding 转换时,Cipher.update 拒绝将数据输出到提供的缓冲区,并返回写入的 0 个字节。缓冲区在密码内部建立,直到我调用 .doFinal。这似乎也发生在 CCM 中(我假设其他身份验证模式),但适用于其他模式,如 CBC。

我认为 GCM 可以在加密时计算身份验证标签,所以我不确定为什么不允许我使用 Cipher 中的缓冲区。

我做了一个例子,只调用 .update: (kotlin)

val secretKey = KeyGenerator.getInstance("AES").run {
    init(256)
    generateKey()
}

val iv = ByteArray(12)
SecureRandom().nextBytes(iv)

val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))

// Pretend this is some file I want to read and encrypt
val inputBuffer = Random.nextBytes(1024000)

val outputBuffer = ByteArray(cipher.getOutputSize(512))

val read = cipher.update(inputBuffer, 0, 512, outputBuffer, 0)
//   ^  at this point, read = 0 and outputBuffer is [0, 0, 0, ...]
// Future calls to cipher.update and cipher.getOutputSize indicate that
// the internal buffer is growing. But I would like to consume it through
// outputBuffer

// ...

cipher.doFinal(outputBuffer, 0)
// Now outputBuffer is populated

我想做的是从磁盘流式传输一个大文件,对其进行加密并逐块通过网络发送,而无需将整个文件数据加载到内存中。我尝试使用 CipherInputStream,但遇到了同样的问题。

这可以通过 AES/GCM 实现吗?

【问题讨论】:

  • 我无法复制。在调用 cipher.update() 后立即打印 read 的值会打印 512。将块写入 ByteArrayOutputStream 并打印 baos.toByteArray() 的长度也显示每次迭代的大小都在增加。我不得不删除 IvParameterSpec,因为它会导致 java.security.InvalidAlgorithmParameterException。
  • 有趣...也许我应该澄清这是在 Android 上,如果这可能会影响它的实现方式。将更新我的问题以反映这一点。
  • 这是 GCM 模式的“功能”。解密时,明文被“禁运”,直到调用doFinal 并且可以验证标签。原因很简单:如果update()一生成明文就将其交还给您,而最后标签无法验证,则您收到的明文将无效。 doFinal() 将无法召回有缺陷的明文,您将无法根据该有缺陷的明文召回您刚刚发射的核导弹。
  • 不幸的是,这是图书馆设计师的选择。请参阅 answer 下的 Maarten 评论。如果您愿意,您可以划分和链接您的数据。
  • 我将继续在conscrypt issues 上打开一个问题。几乎可以肯定它会被忽略。同时,我怀疑其他提供商(例如 bouncycastle)会产生预期的结果。但是,默认 Conscrypt 提供程序的总吞吐量可能仍然更快,因为它使用本机代码。这是您必须做出的权衡。

标签: java android encryption kotlin aes-gcm


【解决方案1】:

这是由 Android 现在默认使用的 Conscrypt 提供程序的限制引起的。这是一个代码示例,我在不是 Android 上运行,而是在显式使用 Conscrypt 提供程序的 Mac 上运行,接下来使用 Bouncycastle (BC) 提供程序来显示差异。因此,一种解决方法是将 BC 提供程序添加到您的 Android 项目中,并在调用 Cipher.getInstance() 时明确指定它。当然,有一个权衡。虽然 BC 提供程序会在每次调用 update() 时向您返回密文,但总体吞吐量可能会大大降低,因为 Conscrypt 使用本机库并且 BC 是纯 Java。

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.conscrypt.Conscrypt;

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.GeneralSecurityException;
import java.security.Provider;
import java.security.SecureRandom;
import java.security.Security;

public class ConscryptIssue1 {

    private final static Provider CONSCRYPT = Conscrypt.newProvider();
    private final static Provider BC = new BouncyCastleProvider();

    public static void main(String[] args) throws GeneralSecurityException {
        Security.addProvider(CONSCRYPT);
        doExample();
    }

    private static void doExample() throws GeneralSecurityException {
        final SecureRandom secureRandom = new SecureRandom();
        {
            // first, try with Conscrypt
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(256, secureRandom);
            SecretKey aesKey = keyGenerator.generateKey();
            byte[] plaintext = new byte[10000]; // plaintext is all zeros
            byte[] nonce = new byte[12];
            secureRandom.nextBytes(nonce);
            Cipher c = Cipher.getInstance("AES/GCM/NoPadding", CONSCRYPT);// specify the provider explicitly
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce);// tag length is specified in bits.
            c.init(Cipher.ENCRYPT_MODE, aesKey, spec);
            byte[] outBuf = new byte[c.getOutputSize(512)];
            int numProduced = c.update(plaintext, 0, 512, outBuf, 0);
            System.out.println(numProduced);
            final int finalProduced = c.doFinal(outBuf, numProduced);
            System.out.println(finalProduced);
        }

        {
            // Next, try with Bouncycastle
            KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
            keyGenerator.init(256, secureRandom);
            SecretKey aesKey = keyGenerator.generateKey();
            byte[] plaintext = new byte[10000]; // plaintext is all zeros
            byte[] nonce = new byte[12];
            secureRandom.nextBytes(nonce);
            Cipher c = Cipher.getInstance("AES/GCM/NoPadding", BC);// specify the provider explicitly
            GCMParameterSpec spec = new GCMParameterSpec(128, nonce);// tag length is specified in bits.
            c.init(Cipher.ENCRYPT_MODE, aesKey, spec);
            byte[] outBuf = new byte[c.getOutputSize(512)];
            int numProduced = c.update(plaintext, 0, 512, outBuf, 0);
            System.out.println(numProduced);
            final int finalProduced = c.doFinal(outBuf, numProduced);
            System.out.println(finalProduced);
        }

    }
}

【讨论】:

  • 感谢您对此进行如此详细的调查。关于 CCM,这是我的错误,事实证明 Cipher 在指定 CCM 时选择了 Android 上的内置 BC 提供程序,在这种情况下我现在似乎无法复制这种行为。我会考虑将 GCM 与 BouncyCastle 一起使用,或者可能将 CTR+HMAC 与 Conscrypt 一起使用,它确实提供了来自 update() 的输出
  • '限制'?错误?
  • 有趣的是,如果我明确调用Cipher.getInstance("AES/GCM/NoPadding", "BC"),Android 会抱怨内置的 BC 不再支持 AES/GCM,但如果我没有在 @987654326 中指定提供程序,我可以强制它使用 BC @ 但将 nonce 大小更改为 12 以外的值(我不想这样做)。
  • @user207421:它返回正确的结果并符合 Javadocs,只是以最不令人满意的方式这样做。
  • @Will:IIRC BC 提供程序不再受 Android 支持,除了一些向后兼容性,但您可以将 BC jar 添加到您的 Android 项目依赖项中。在尝试使用它之前,请确保您执行Security.removeProvider("BC"); Security.addProvider(new BouncyCastleProvider());。
【解决方案2】:

如果有人正在寻找与此相反的东西。 (没有输出,只是一个标签)cipher.updateAAD(src) 有效...花了我两天时间才找到它,但它有效

【讨论】:

    猜你喜欢
    • 2018-07-31
    • 2015-11-23
    • 2023-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-04
    • 1970-01-01
    • 2013-01-05
    相关资源
    最近更新 更多