2
votes

I am trying to figure out how to read data that was encoded with AES/GCM/NoPadding. The data that I am working with will be arbitrarily large, and I'm hoping to read it in chunks, but I'm having difficulties figuring out how that would be accomplished. Here is an example of where I'm at right now:

@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"));
}

Note that I get past the first assert no problem, so the data can be decoded in a single go. Also, the next two sets of asserts (decoding the first 32 bytes in 16-byte chunks) work "correctly" but I arrived at this formula through trial-and-error. There are a few things about them that I don't understand:

  • Even though I am reading in 16-byte chunks, all of my numbers seem to need to be multiples of 32. If I change to the following code, then the first call to cipher.update() fails with a return value of 0.

    byte[] first16 = Arrays.copyOfRange(ciphertext, 0, 16);
    byte[] decryptedChunk = new byte[16];
    
    int num = cipher.update(first16, 0, 16, decryptedChunk);
    
  • If I change back to 32 on the input side, but I work with a 16-byte output buffer, then the first call succeeds and returns the expected data, but the second call to cipher.update() throws 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);
    
  • So, if I change the code back to my original example (decryptedChunk is sized at 32 bytes) then the third call to cipher.update() returns a value of 16 (meaning what???) and decryptedChunk contains garbage data.

  • I also tried replacing the last call to cipher.update() with a call to cipher.doFinal() instead:

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

But this fails with a BadPaddingException (mac check in GCM failed).

Any suggestions?


Update with solution

After playing around some with the suggested code from Ebbe M. Pedersen, I have been able to put together the following solution:

@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) + ">");
}

This works properly for any value of chunkSize that I have selected. I have marked that answer as accepted. Thank you all for the help.

1
How do you expect different output from cipher.update(first32, 0, 32, decryptedChunk) when you call it twice in a row with exactly same arguments? - Oleg Estekhin
And you also should re-initialize the cipher after the first call doFinal(). - Oleg Estekhin
Adding cipher.init() after the first call to doFinal() makes no difference. I have tried it. Furthermore, according to the Oracle documentation for doFinal(), Upon finishing, this method resets this cipher object to the state it was in when previously initialized via a call to init. That is, the object is reset and available to encrypt or decrypt (depending on the operation mode that was specified in the call to init) more data. - danBhentschel
I have tried various different inputs for the second call: cipher.update(first32, 16, 32, decryptedChunk) makes the most sense to me. It throws IllegalArgumentException. cipher.update(first32, 32, 32, decryptedChunk) throws the same error, as does cipher.update(first32, 32, 64, decryptedChunk). Am I missing an option? On the other hand, if I just repeat the original call cipher.update(first32, 0, 32, decryptedChunk), it actually does return the next 16 bytes of data. I wasn't expecting this behavior, but as I mentioned, I found this to work through trial and error. - danBhentschel
Somewhat orthogonal to the question, but it's worth reminding: be careful with partial/streaming GCM decryptions. Until the whole stream has been decrypted and the MAC checked, none of it has been authenticated. - Peter Dettman

1 Answers

3
votes

Block ciphers [ed: in Bouncy Castle] have an internal buffer that they keep updating, and only when they have enough data for a full block, will the decrypt occur, and a chunk of the decrypted data be returned.

You can see this if you try and decrypt it 1 byte at a time like this:

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

This gives the following output with your encrypted data:

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

Notice that I need to do a doFinal(), to get the last piece of data out.


Note that this is particular to the Bouncy Castle implementation, at least up to version 1.50. CTR mode allows to pre-compute blocks of the key stream used to encrypt/decrypt the data (by XOR'ing, analog of a OTP encryption). So in principle each byte or even bit can be encrypted/decrypted on its own.