DoFinal: примеры (JAVA)
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)
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 с выходным буфером и смещением - повторное использование буфера
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 для каждого блока
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 в заранее выделенный буфер
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 и прямыми буферами для нативной скорости
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);
Результат: длина зашифрованных данных
Комментарий: прямые буферы помогают снизить копирования при взаимодействии с нативными провайдерами.