2

我试图弄清楚如何读取用 AES/GCM/NoPadding 编码的数据。我正在使用的数据将任意大,我希望分块阅读它,但我很难弄清楚如何完成。这是我现在所处位置的示例:

@Test
public void chunkDecrypt() throws Exception {
    key = MessageDigest.getInstance("MD5").digest("som3C0o7p@s5".getBytes());
    iv = Hex.decode("EECE34808EF2A9ACE8DF72C9C475D751");
    byte[] ciphertext = Hex
            .decode("EF26839493BDA6DA6ABADD575262713171F825F2F477FDBB53029BEADB41928EA5FB46737D7A94D5BE74B6049008443664F0E0D883943D0EFBEA09DB");

    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));

    byte[] fullDecryptedPlainText = cipher.doFinal(ciphertext);
    assertThat(new String(fullDecryptedPlainText),
            is("The quick brown fox jumps over the lazy dogs"));

    byte[] first32 = Arrays.copyOfRange(ciphertext, 0, 32);
    byte[] final28 = Arrays.copyOfRange(ciphertext, 32, 60);
    byte[] decryptedChunk = new byte[32];

    int num = cipher.update(first32, 0, 32, decryptedChunk);
    assertThat(num, is(16));
    assertThat(new String(decryptedChunk, 0, 16), is("The quick brown "));

    num = cipher.update(first32, 0, 32, decryptedChunk);
    assertThat(num, is(32));
    assertThat(new String(decryptedChunk, 0, 16), is("fox jumps over t"));

    num = cipher.update(final28, 0, 24, decryptedChunk);
    assertThat(num, is(44));
    assertThat(new String(decryptedChunk, 0, 12), is("he lazy dogs"));
}

请注意,我通过第一个断言没有问题,因此可以一次性解码数据。此外,接下来的两组断言(解码 16 字节块中的前 32 个字节)“正确”工作,但我通过反复试验得出了这个公式。他们有一些我不明白的地方:

  • 即使我正在阅读 16 字节的块,我的所有数字似乎都需要是 32 的倍数。如果我更改为以下代码,那么对 cipher.update() 的第一次调用将失败,返回值为 0。

    byte[] first16 = Arrays.copyOfRange(ciphertext, 0, 16);
    byte[] decryptedChunk = new byte[16];
    
    int num = cipher.update(first16, 0, 16, decryptedChunk);
    
  • 如果我在输入端改回 32,但我使用 16 字节的输出缓冲区,那么第一次调用会成功并返回预期的数据,但对 cipher.update() 的第二次调用会引发 ArrayIndexOutOfBoundsException。

    byte[] first32 = Arrays.copyOfRange(ciphertext, 0, 32);
    byte[] decryptedChunk = new byte[16];
    
    int num = cipher.update(first32, 0, 32, decryptedChunk);
    num = cipher.update(first32, 0, 32, decryptedChunk);
    
  • 因此,如果我将代码改回原来的示例(decryptedChunk 大小为 32 字节),那么对 cipher.update() 的第三次调用返回值 16(意味着什么???),并且 decryptedChunk 包含垃圾数据。

  • 我还尝试用对 cipher.doFinal() 的调用代替对 cipher.update() 的最后调用:

    decryptedChunk = cipher.doFinal(final28);
    assertThat(new String(decryptedChunk, 0, 12), is("he lazy dogs"));
    

但这会因 BadPaddingException 而失败(GCM 中的 mac 检查失败)。

有什么建议么?


更新解决方案

在使用了 Ebbe M. Pedersen 的建议代码后,我已经能够整理出以下解决方案:

@Test
public void chunkDecrypt() throws Exception {
    byte[] key = MessageDigest.getInstance("MD5").digest("som3C0o7p@s5".getBytes());
    byte[] iv = Hex.decode("EECE34808EF2A9ACE8DF72C9C475D751");
    byte[] ciphertext = Hex
            .decode("EF26839493BDA6DA6ABADD575262713171F825F2F477FDBB53029BEADB41928EA5FB46737D7A94D5BE74B6049008443664F0E0D883943D0EFBEA09DB");

    Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding", "BC");
    cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));

    int chunkSize = 16;
    byte[] inBuffer = new byte[chunkSize];
    int outBufferSize = ((chunkSize + 15) / 16) * 16;
    byte[] outBuffer = new byte[outBufferSize];

    for (int i = 0; i < ciphertext.length; i += chunkSize) {
        int thisChunkSize = Math.min(chunkSize, ciphertext.length - i);
        System.arraycopy(ciphertext, i, inBuffer, 0, thisChunkSize);
        int num = cipher.update(inBuffer, 0, thisChunkSize, outBuffer);
        if (num > 0) {
            logger.debug("update #" + ((i / chunkSize) + 1) + " - data <"
                    + new String(outBuffer, 0, num) + ">");
        }
    }
    int num = cipher.doFinal(inBuffer, chunkSize, 0, outBuffer);
    logger.debug("doFinal - data <" + new String(outBuffer, 0, num) + ">");
}

这适用于chunkSize我选择的任何值。我已将该答案标记为已接受。谢谢大家的帮助。

4

1 回答 1

3

块密码 [ed: in Bouncy Castle] 有一个内部缓冲区,它们会不断更新,只有当它们有足够的数据用于一个完整的块时,才会发生解密,并返回一大块解密的数据。

如果您尝试一次解密 1 个字节,您可以看到这一点,如下所示:

    byte[] buffer = new byte[32];
    for (int i = 0; i < ciphertext.length; i++) {
        int num = cipher.update(ciphertext, i, 1, buffer);
        if (num > 0) {
            System.out.println("update #" + (i + 1) + " - data <" + new String(buffer, 0, num) + ">");
        }
    }
    int num = cipher.doFinal(ciphertext, ciphertext.length, 0, buffer);
    System.out.println("doFinal - data <" + new String(buffer, 0, num) + ">");

这会为您的加密数据提供以下输出:

update #32 - data <The quick brown >
update #48 - data <fox jumps over t>
doFinal - data <he lazy dogs>

请注意,我需要执行一个 doFinal() 来获取最后一条数据。


请注意,这是 Bouncy Castle 实现所特有的,至少在 1.50 版之前是这样。CTR 模式允许预先计算用于加密/解密数据的密钥流块(通过 XOR'ing,类似于 OTP 加密)。所以原则上每个字节甚至比特都可以自己加密/解密。

于 2014-06-20T20:07:27.640 回答