Чтение данных в кодировке AES/GCM порциями с помощью BouncyCastle в Java

Я пытаюсь выяснить, как читать данные, которые были закодированы с помощью 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"));
}

Обратите внимание, что после первого утверждения я не вижу проблем, поэтому данные могут быть декодированы за один раз. Кроме того, следующие два набора утверждений (декодирование первых 32 байтов в 16-байтовых чанках) работают "правильно", но я пришел к этой формуле методом проб и ошибок. В них есть несколько вещей, которые я не понимаю:

  • Несмотря на то, что я читаю 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.update() на вызов cipher.doFinal() вместо этого:

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

Но это происходит с ошибкой BadPaddingException (проверка Mac в GCM не удалась).

Какие-либо предложения?


Обновление с решением

Поработав с предложенным кодом от 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 что я выбрал. Я отметил этот ответ как принятый. Спасибо всем за помощь.

1 ответ

Решение

Блочные шифры [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-шифрования). Таким образом, в принципе каждый байт или даже бит может быть зашифрован / дешифрован самостоятельно.

Другие вопросы по тегам