Java Cipher.update 使用 AES/GCM 时不写入缓冲区 (Android 9)

Java Cipher.update does not write to buffer when using AES/GCM (Android 9)

我正在尝试在 Android 上使用 javax.crypto.Cipher 来使用 AES-GCM 以块的形式加密数据流。据我了解,可以多次使用 Cipher.update 进行多部分加密操作,并使用 Cipher.doFinal 完成。但是,当使用 AES/GCM/NoPadding 转换时,Cipher.update 拒绝将数据输出到提供的缓冲区,并且 returns 写入了 0 个字节。在我调用 .doFinal 之前,缓冲区会在 Cipher 内部累积。这似乎也发生在 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可以吗?

这是由 Android 现在默认使用的 Conscrypt 提供程序的限制引起的。这是一个代码示例,我 运行 不是 而是 Android 而是在我的 Mac 上,它显式使用了 Conscrypt 提供程序,接下来使用 Bouncycastle (BC) 提供程序来显示差异。因此,解决方法是将 BC 提供程序添加到您的 Android 项目并在调用 Cipher.getInstance() 时明确指定它。当然,需要权衡取舍。虽然 BC 提供商会在每次调用 update() 时向您发送 return 密文,但总体吞吐量可能会大大减少,因为 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);
        }

    }
}

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