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

#java #encryption #bouncycastle #aes-gcm

#java #шифрование #bouncycastle #aes-gcm

Вопрос:

Я пытаюсь выяснить, как считывать данные, которые были закодированы с помощью 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. Как вы ожидаете, что результат будет отличаться от cipher.update(first32, 0, 32, decryptedChunk) того, когда вы вызываете его дважды подряд с точно такими же аргументами?

2. И вам также следует повторно инициализировать шифр после первого вызова doFinal() .

3. Добавление cipher.init() после первого вызова doFinal() не имеет значения. Я пробовал это. Кроме того, согласно документации Oracle для doFinal(), По завершении этот метод сбрасывает этот объект шифрования в состояние, в котором он находился при предыдущей инициализации с помощью вызова init . То есть объект сбрасывается и становится доступным для шифрования или дешифрования (в зависимости от режима работы, который был указан при вызове init) дополнительных данных.

4. Я пробовал разные разные входные данные для второго вызова: cipher.update(first32, 16, 32, decryptedChunk) для меня это имеет наибольший смысл. Это вызывает исключение IllegalArgumentException. cipher.update(first32, 32, 32, decryptedChunk) выдает ту же ошибку, что и does cipher.update(first32, 32, 64, decryptedChunk) . Я пропустил какой-то вариант? С другой стороны, если я просто повторю исходный вызов cipher.update(first32, 0, 32, decryptedChunk) , он фактически вернет следующие 16 байт данных. Я не ожидал такого поведения, но, как я уже упоминал, я обнаружил, что это работает методом проб и ошибок.

5. Несколько ортогонально вопросу, но стоит напомнить: будьте осторожны с частичными / потоковыми расшифровками GCM. Пока весь поток не будет расшифрован и не будет проверен MAC, ни один из них не был аутентифицирован.

Ответ №1:

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

Комментарии:

1. Для бокового шифра в потоковом режиме каждый байт может быть расшифрован самостоятельно. Обычно блок потока ключей создается, когда требуется расшифровка, каждый байт может быть зашифрован или расшифрован самостоятельно. Конечно, если это выполняется таким образом, зависит от реализации. В принципе, большие части или даже весь поток ключей могут быть предварительно сгенерированы.

2. @owlstead Совершенно верно, но просто для ясности — реализация BC (в настоящее время) предлагает только блочное шифрование / дешифрование для GCM.

3. @PeterDettman отредактировал ответ, чтобы показать, что это верно для данной конкретной реализации. Спасибо, что заглянули (в качестве сопровождающего BC)!

4. Это немного приближает меня к решению, но предполагает, что у меня будет все ciphertext в одном монолитном буфере. Это не то, к чему я стремился. Если я изменю вышеупомянутое решение, чтобы извлекать меньшие буферы из ciphertext блоков с переменными размерами, то дешифрование всегда прерывается с помощью an IllegalArgumentException в то время, когда мой буфер запускается, независимо от того, какой размер блока я выбираю. Возможно, невозможно сделать то, что я хочу сделать с GCM? На данный момент я переключился на использование CTR с дайджестом SHA-256 в конце зашифрованного потока.

5. Обновление: после небольшого исследования я почти уверен, что зашифрованный дайджест CTR — неправильный путь, и вместо этого я завершу свой поток CTR с помощью HmacSHA256 MAC.