DoFinal: примеры (JAVA)

Методы doFinal в Java и демонстрации
Раздел: Безопасность (Security), Криптография
doFinal(byte[] input): byte[]

Общее описание doFinal в Java

В Java метод doFinal используется в криптографических API для завершения обработки данных и получения окончательного результата операции шифрования, расшифровки или вычисления кода аутентификации. В JDK чаще всего встречаются реализации в классах Cipher и Mac. Метод завершает накопленную внутреннюю буферизацию, выполняет окончательное выравнивание с учётом схемы паддинга и возвращает готовые байты.

Типичный набор перегрузок для Cipher.doFinal включает:

  • byte[] doFinal() - завершает операцию и возвращает результат как новый массив байт.
  • byte[] doFinal(byte[] input) - завершает, одновременно добавив входные данные, возвращает новый массив байт.
  • byte[] doFinal(byte[] input, int inputOffset, int inputLen) - как предыдущий, с указанием подотрезка входного массива.
  • int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output) - записывает результат в заранее выделенный буфер output, возвращает количество записанных байт. Может бросить ShortBufferException при недостаточном размере выходного буфера.
  • int doFinal(byte[] input, int inputOffset, int inputLen, byte[] output, int outputOffset) - запись с указанием смещения в выходном массиве.
  • int doFinal(ByteBuffer input, ByteBuffer output) - читает из и пишет в ByteBuffer, возвращает количество записанных байт.

У класса Mac перегрузки чуть проще:

  • byte[] doFinal() - вычисляет MAC для накопленных данных и возвращает массив.
  • byte[] doFinal(byte[] input) - добавляет данные и возвращает MAC.
  • int doFinal(byte[] output, int outOffset) - записывает MAC в предоставленный буфер, возвращает количество байт.

Возвращаемые значения:

  • массив байт с результатом (для версий, возвращающих byte[]);
  • целое число - количество записанных байт при записи в предоставленный буфер.

Типичные исключения:

  • IllegalBlockSizeException - данные не соответствуют блочному режиму без паддинга;
  • BadPaddingException - неверный паддинг при расшифровке (часто означает неверный ключ или поврёжденные данные);
  • ShortBufferException - выходной буфер слишком мал;
  • IllegalStateException - неустановленная операция (например, не вызван init или объект уже закрыт).

Когда применяется

Использование совпадает с моментом, когда нужно завершить криптооперацию: после всех update-вызовов, перед получением окончательного зашифрованного блока или MAC. Для одношаговых входных данных часто вызывается версия с передачей массива сразу в doFinal(input).

Короткие примеры использования doFinal

1. AES/CBC/PKCS5Padding - шифрование и расшифровка

// Шифрование (Java)
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKeySpec key = new SecretKeySpec(keyBytes, "AES");
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
byte[] ciphertext = cipher.doFinal(plaintextBytes);
String b64 = Base64.getEncoder().encodeToString(ciphertext);
System.out.println(b64);
Результат: Base64 строка, например "mYV1...=="
// Расшифровка (Java)
cipher.init(Cipher.DECRYPT_MODE, key, iv);
byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(b64));
System.out.println(new String(decrypted, StandardCharsets.UTF_8));
Результат: исходный текст

2. Использование заранее выделенного буфера

byte[] out = new byte[128];
int len = cipher.doFinal(input, 0, input.length, out);
byte[] result = Arrays.copyOf(out, len);
System.out.println(result.length);
Результат: количество байт, например 48

3. HMAC с Mac.doFinal

Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(keyBytes, "HmacSHA256"));
mac.update(dataPart1);
mac.update(dataPart2);
byte[] tag = mac.doFinal();
System.out.println(Base64.getEncoder().encodeToString(tag));
Результат: HMAC в Base64, например "X1x...=="

4. ByteBuffer вариант

ByteBuffer in = ByteBuffer.wrap(plaintextBytes);
ByteBuffer outBuf = ByteBuffer.allocate(256);
int written = cipher.doFinal(in, outBuf);
outBuf.flip();
byte[] enc = new byte[outBuf.remaining()];
outBuf.get(enc);
System.out.println(Base64.getEncoder().encodeToString(enc));
Результат: Base64 строка с зашифрованными данными

Альтернативы в Java и их особенности

Внутри JDK есть несколько методов, которые используются в смежных сценариях:

  • Cipher.update - выполняет обработку доступных байтов, но не завершает паддинг. При больших потоках данных часто комбинируется с doFinal() для завершения.
  • Cipher.wrap / Cipher.unwrap - специализированы для упаковки и распаковки ключей; внутри используют похожие механизмы, но ориентированы на ключи.
  • Signature.sign / Signature.verify - для цифровой подписи; применяются вместо MAC/шифрования, когда нужна асимметричная подпись.
  • MessageDigest.digest - вычисляет хеш, аналогичен по концепции Mac.doFinal, но без секретного ключа и с другой семантикой.

Выбор зависит от задачи: для симметричного шифрования - Cipher с doFinal; для кода аутентификации - Mac; для подписи - Signature. Если требуется потоковая обработка больших данных с минимальными копиями, лучше использовать обновления через update и запись в заранее выделенные буферы или потоковые обёртки.

Аналоги в других языках и отличия

Краткий обзор популярных альтернатив и отличий от Java:

  • Node.js (crypto)
    const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
    let enc = cipher.update(plain, 'utf8', 'base64');
    enc += cipher.final('base64');
    console.log(enc);
    Результат: Base64-строка
    Отличие: разделение на update и final по аналогии с Java, но API возвращает строки в указанных кодировках.
  • Python (cryptography)
    from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
    cipher = Cipher(algorithms.AES(key), modes.CBC(iv))
    enc = cipher.encryptor()
    ciphertext = enc.update(plaintext) + enc.finalize()
    print(base64.b64encode(ciphertext))
    Результат: Base64 байты
    Отличие: метод называется finalize(), поведение схоже, есть отдельные контексты для шифрования/дешифрования.
  • PHP (OpenSSL)
    $ciphertext = openssl_encrypt($plaintext, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv);
    echo base64_encode($ciphertext);
    Результат: Base64 строка
    Отличие: функционал объединён в одну функцию без явного doFinal, управление паддингом через флаги.
  • C# (.NET)
    using(var transform = aes.CreateEncryptor())
    {
        byte[] result = transform.TransformFinalBlock(plain, 0, plain.Length);
        Console.WriteLine(Convert.ToBase64String(result));
    }
    Результат: Base64 строка
    Отличие: метод называется TransformFinalBlock, семантика аналогична doFinal.
  • Go
    // Для AEAD
    ciphertext := aead.Seal(nil, nonce, plaintext, nil)
    fmt.Println(base64.StdEncoding.EncodeToString(ciphertext))
    Результат: Base64 строка
    Отличие: Go часто использует AEAD.Seal/Open без явного этапа finalize - итоговая функция возвращает завершающий результат.
  • Kotlin - использует те же Java API, что и Java, различия синтаксические.
  • Lua - расширения сторонних библиотек; API различаются и чаще объединяют обновление и финализацию в одном вызове.

Во всех языках идея одна: обработать блоки данных и выполнить финальную операцию, но конкретные названия и варианты передачи буферов отличаются. Java предоставляет богатый набор перегрузок для управления копированиями и буферами.

Типичные ошибки и сценарии

1. BadPaddingException при расшифровке

Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.DECRYPT_MODE, wrongKey, iv);
byte[] plain = cipher.doFinal(ciphertext);
Исключение: javax.crypto.BadPaddingException: Given final block not properly padded

Причина: неверный ключ, повреждённые данные или использование неправильного режима/паддинга.

2. IllegalBlockSizeException при отсутствии паддинга

Cipher c = Cipher.getInstance("AES/ECB/NoPadding");
c.init(Cipher.ENCRYPT_MODE, key);
byte[] out = c.doFinal(new byte[3]);
Исключение: javax.crypto.IllegalBlockSizeException: Input length not multiple of 16

Причина: входной размер не кратен размеру блока и паддинг отключён.

3. ShortBufferException при записи в маленький буфер

byte[] out = new byte[1];
cipher.doFinal(input, 0, input.length, out);
Исключение: javax.crypto.ShortBufferException

Решение: выделение достаточного буфера или использование перегрузки, возвращающей массив.

4. IllegalStateException при неправильном порядке вызовов

Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
// пропущен init
c.doFinal(data);
Исключение: java.lang.IllegalStateException: Cipher not initialized

Причина: не вызван init или объект используется некорректно.

Изменения и эволюция doFinal

За время развития JDK API улучшения в основном касались удобства работы с буферами и производительности:

  • появление перегрузок, позволяющих записывать результат в заранее выделённый массив, снизило количество аллокаций при интенсивной работе;
  • поддержка ByteBuffer облегчила интеграцию с NIO и уменьшила копирования при использовании прямых буферов;
  • поправки и оптимизации реализации в провайдерах, направленные на безопасность и устойчивость к утечкам информации через побочные каналы.

Рекомендация: при обновлении JDK проверять релиз-нотсы провайдеров криптографии и следить за объявленными улучшениями производительности и безопасности, так как конкретные изменения зависят от реализации провайдера и версии JDK.

Расширенные и редкие примеры

1. Шифрование больших потоков с минимальными копиями (update + doFinal)

Пример java
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] buff = new byte[4096];
int read;
// чтение из InputStream
while ((read = in.read(buff)) != -1) {
    byte[] part = cipher.update(buff, 0, read);
    if (part != null) out.write(part);
}
byte[] last = cipher.doFinal();
if (last != null) out.write(last);
byte[] ciphertext = out.toByteArray();
System.out.println(ciphertext.length);
Результат: число байт, размер шифротекста

Комментарий: уменьшение числа копирований за счёт последовательных update и единственного doFinal.

2. doFinal с выходным буфером и смещением - повторное использование буфера

Пример java
byte[] buf = new byte[1024];
int off = 0;
int written = cipher.doFinal(input, 0, input.length, buf, off);
// следующий вызов может использовать ту же область дальше
off += written;
System.out.println(off);
Результат: итоговое смещение, например 64

Комментарий: подходит для упаковки нескольких результатов подряд в один массив без дополнительных копирований.

3. RSA с разбивкой блоков и doFinal для каждого блока

Пример java
Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
rsa.init(Cipher.ENCRYPT_MODE, pubKey);
int blockSize = 190; // зависит от ключа и OAEP
ByteArrayOutputStream baos = new ByteArrayOutputStream();
for (int pos = 0; pos < plain.length; pos += blockSize) {
    int len = Math.min(blockSize, plain.length - pos);
    baos.write(rsa.doFinal(plain, pos, len));
}
byte[] full = baos.toByteArray();
System.out.println(full.length);
Результат: длина зашифрованных блоков, кратная размеру ключа

Комментарий: при использовании асимметричных алгоритмов требуются блоковые операции.

4. Использование doFinal для получения MAC в заранее выделенный буфер

Пример java
Mac mac = Mac.getInstance("HmacSHA1");
mac.init(new SecretKeySpec(key, "HmacSHA1"));
mac.update(data);
byte[] out = new byte[mac.getMacLength() + 10];
int len = mac.doFinal(out, 5); // записать с отступом
System.out.println(len);
Результат: длина MAC, например 20

Комментарий: запись в существующий буфер позволяет экономить аллокации и размещать метки рядом с MAC.

5. Пример с ByteBuffer и прямыми буферами для нативной скорости

Пример java
ByteBuffer in = ByteBuffer.allocateDirect(8192);
ByteBuffer out = ByteBuffer.allocateDirect(8192 + 64);
in.put(plaintext);
in.flip();
int wrote = cipher.doFinal(in, out);
out.flip();
byte[] res = new byte[out.remaining()];
out.get(res);
System.out.println(res.length);
Результат: длина зашифрованных данных

Комментарий: прямые буферы помогают снизить копирования при взаимодействии с нативными провайдерами.

джава doFinal function comments

En
DoFinal Finishes the encryption or decryption operation