Cipher.doFinal: примеры (JAVA)
Cipher.doFinal(byte[] input): byte[]Описание Cipher.doFinal и параметры
Метод Cipher.doFinal из пакета javax.crypto завершает операцию шифрования или расшифровки и возвращает финальный блок данных. Обычно используется после последовательных вызовов update для получения оставшихся данных и выполнения паддинга или проверки тегов целостности для режимов AEAD.
Существуют несколько перегрузок. Краткое описание:
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(java.nio.ByteBuffer input, java.nio.ByteBuffer output)- обрабатывает данные из входногоByteBufferи записывает в выходной; возвращает число записанных байт.
Возвращаемое значение: либо массив байт (если используется вариант без внешнего буфера), либо целое число записанных байт (если результат пишется в предоставленный буфер).
Исключения, наиболее релевантные для doFinal:
IllegalBlockSizeException- размер входных данных не кратен блоку и паддинг отсутствует или неподходящий режим.BadPaddingException- некорректный паддинг при расшифровке (чаще всего признак неверного ключа или повреждённых данных).ShortBufferException- выходной буфер слишком мал для записи результата.AEADBadTagException- для AEAD-режимов (например, GCM) некорректная метка аутентификации.
Рекомендация по использованию: перед вызовом doFinal часто имеет смысл вызвать getOutputSize(int inputLen) для оценки необходимого размера выходного буфера, особенно при использовании вариантов с заранее выделённым массивом.
Короткие примеры применения
Примеры показывают основные варианты: простая шифровка/расшифровка, запись в заранее выделённый буфер и работа с ByteBuffer.
Пример 1: AES/CBC/PKCS5Padding - простая шифровка и расшифровка
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.util.Base64;
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(128);
SecretKey key = kg.generateKey();
byte[] iv = new byte[16];
java.security.SecureRandom.getInstanceStrong().nextBytes(iv);
Cipher enc = Cipher.getInstance("AES/CBC/PKCS5Padding");
enc.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] ciphertext = enc.doFinal("hello world".getBytes());
Cipher dec = Cipher.getInstance("AES/CBC/PKCS5Padding");
dec.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] plaintext = dec.doFinal(ciphertext);
System.out.println(Base64.getEncoder().encodeToString(ciphertext));
System.out.println(new String(plaintext));
(пример вывода) mZ7q3v1Z1x2VvY5y2QwX6w== hello world
Пример 2: запись результата в заранее выделённый буфер
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] in = "data to encrypt".getBytes();
int outSize = cipher.getOutputSize(in.length);
byte[] out = new byte[outSize + 10]; // дополнительное место
int len = cipher.doFinal(in, 0, in.length, out, 5); // смещение 5
// фактический результат находится в out[5 .. 5+len-1]
System.out.println(len + " bytes");
(пример вывода) 32 bytes
Пример 3: ByteBuffer-версия
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, key);
java.nio.ByteBuffer inBuf = java.nio.ByteBuffer.wrap("text for gcm".getBytes());
java.nio.ByteBuffer outBuf = java.nio.ByteBuffer.allocate(c.getOutputSize(inBuf.remaining()));
int written = c.doFinal(inBuf, outBuf);
outBuf.flip();
byte[] res = new byte[outBuf.remaining()];
outBuf.get(res);
System.out.println(written);
System.out.println(java.util.Base64.getEncoder().encodeToString(res));
(пример вывода) 28 qwerty...base64...
Похожие механизмы в Java и их особенности
- Cipher.update - возвращает промежуточный фрагмент результата; не завершает операцию и не выполняет паддинг. Используется при потоковой обработке больших объёмов.
- CipherInputStream / CipherOutputStream - удобны для потоковой обработки файлов и сетевых потоков; скрывают вызовы
updateиdoFinalвнутри себя. - Mac - для вычисления кода аутентичности сообщений; не заменяет шифрование, но используется совместно для целостности.
- AEAD режимы (GCM) - требуют вызова
updateAADперед шифрованием/расшифровкой и затемdoFinalдля проверки тега; при проверке некорректный тег вызывает AEAD-исключение.
Выбор между этими вариантами определяется задачей: для блочного шифрования небольших буферов удобен прямой вызов doFinal; для потоков и больших объёмов лучше использовать CipherInputStream/CipherOutputStream или сочетание update + doFinal.
Аналоги в других языках и их отличия
- PHP - функции
openssl_encrypt/openssl_decrypt. Эти функции объединяют процесс в один вызов, похожи на вариантdoFinal(byte[]). Пример:$data = 'hello'; $key = random_bytes(16); $iv = random_bytes(openssl_cipher_iv_length('aes-128-cbc')); $enc = openssl_encrypt($data, 'aes-128-cbc', $key, OPENSSL_RAW_DATA, $iv); echo base64_encode($enc);(пример вывода) YWJj... (base64)
- JavaScript (Web Crypto API) - асинхронный API:
subtle.encrypt/subtle.decrypt, возвращают Promise с ArrayBuffer. Отличается асинхронной моделью и невозможностью прямого доступа к padding-исключениям без перехвата ошибок.// пример const enc = await crypto.subtle.encrypt({name: 'AES-CBC', iv}, key, data); console.log(new Uint8Array(enc));(пример вывода) Uint8Array([...])
- Python (PyCryptodome) - методы
encrypt/decryptу объектов шифра; для режимов с паддингом часто применяются отдельные утилиты pad/unpad. Пример:from Crypto.Cipher import AES cipher = AES.new(key, AES.MODE_CBC, iv) ciphertext = cipher.encrypt(pad(b'hello', 16))(пример вывода) b'\x01\x02...'
- C# (.NET) - классы
AesиICryptoTransform, методыTransformFinalBlockвыполняют роль doFinal. Отличие: более выраженная объектная модель и поточный трансформ интерфейс.var cipher = aes.CreateEncryptor(); var out = cipher.TransformFinalBlock(data, 0, data.Length);(пример вывода) byte[]
- Go (golang) - в стандартной библиотеке есть блочные и режимные реализации. Для CBC используется
cipher.NewCBCEncrypterс ручной обработкой паддинга, нет единого аналога doFinal: паддинг и финальная обработка выполняются вручную.// пример block, _ := aes.NewCipher(key) mode := cipher.NewCBCEncrypter(block, iv) mode.CryptBlocks(ciphertext, paddedPlaintext)(пример вывода) []byte{...} - Kotlin - использует Java Crypto API напрямую, синтаксические отличия минимальны.
- Lua - библиотеки типа luaossl или luacrypto предлагают функции шифрования, чаще объединяющие всю операцию в один вызов.
- SQL - в СУБД часто есть функции AES_ENCRYPT/AES_DECRYPT (MySQL) или PGP_SYM_ENCRYPT (Postgres с расширением); предназначены для простых задач, не заменяют низкоуровневого контроля, который даёт Cipher.
Главное отличие: в Java API имеется несколько перегрузок и контроль над буферами; в других языках чаще используется единый вызов, асинхронный подход или отдельная ручная обработка паддинга.
Типичные ошибки и примеры
- ShortBufferException: возникает при нехватке места в выходном буфере при вызове варианта doFinal с выводом в предоставленный массив.
Cipher c = Cipher.getInstance("AES/ECB/PKCS5Padding"); c.init(Cipher.ENCRYPT_MODE, key); byte[] in = "some data".getBytes(); byte[] out = new byte[4]; try { c.doFinal(in, 0, in.length, out); } catch (javax.crypto.ShortBufferException e) { System.out.println("ShortBuffer"); }ShortBuffer
- BadPaddingException: обычно при расшифровке с неверным ключом или повреждёнными данными.
// неверный ключ при расшифровке Cipher dec = Cipher.getInstance("AES/CBC/PKCS5Padding"); dec.init(Cipher.DECRYPT_MODE, wrongKey, new IvParameterSpec(iv)); try { dec.doFinal(ciphertext); } catch (javax.crypto.BadPaddingException e) { System.out.println("Bad padding or wrong key"); }Bad padding or wrong key
- IllegalBlockSizeException: при использовании блочных алгоритмов без паддинга и с данными, размер которых не кратен блоку.
Cipher c = Cipher.getInstance("AES/ECB/NoPadding"); c.init(Cipher.ENCRYPT_MODE, key); try { c.doFinal("oddlen".getBytes()); } catch (javax.crypto.IllegalBlockSizeException e) { System.out.println("Block size error"); }Block size error
- AEADBadTagException: при проверке GCM-тега. Возникает при повреждении данных или использовании неверного ключа/IV/AAD.
Cipher c = Cipher.getInstance("AES/GCM/NoPadding"); c.init(Cipher.DECRYPT_MODE, key, new javax.crypto.spec.GCMParameterSpec(128, iv)); try { c.doFinal(ciphertext); } catch (javax.crypto.AEADBadTagException e) { System.out.println("Authentication failed"); }Authentication failed
Дополнительно: попытка вызвать doFinal до инициализации cipher приведёт к IllegalStateException или другим ошибкам в зависимости от реализации провайдера. При использовании RSA на больших данных возможен IllegalBlockSizeException - RSA подходит для небольших фрагментов; большие данные предварительно шифруются симметричным ключом (комбинированный подход).
Изменения и эволюция
API Cipher стабилен в течение многих версий Java. В ходе развития появилось несколько удобств:
- Добавление перегрузок, работающих с
ByteBuffer, для более прямой интеграции с NIO и уменьшения копирований. - Появление специализированных исключений для AEAD-режимов, например
AEADBadTagException, что упростило различение ошибок целостности от проблем с паддингом. - Улучшение поддержки современных режимов шифрования (GCM, CTR) и возможностей провайдеров, включая аппаратные ускорения и новые реализации провайдеров безопасности.
Функция doFinal как API не претерпевала радикальных изменений; эволюция касалась дополняющих возможностей и дополнительных перегрузок для удобства работы с буферами.
Расширенные и нетипичные примеры
1) AEAD (GCM) с AAD и проверкой тега
// шифрование
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
byte[] iv = new byte[12];
java.security.SecureRandom.getInstanceStrong().nextBytes(iv);
javax.crypto.spec.GCMParameterSpec spec = new javax.crypto.spec.GCMParameterSpec(128, iv);
c.init(Cipher.ENCRYPT_MODE, key, spec);
byte[] aad = "header-data".getBytes();
c.updateAAD(aad);
byte[] ct = c.doFinal("message".getBytes());
// расшифровка
Cipher d = Cipher.getInstance("AES/GCM/NoPadding");
d.init(Cipher.DECRYPT_MODE, key, spec);
d.updateAAD(aad);
try {
byte[] pt = d.doFinal(ct);
System.out.println(new String(pt));
} catch (javax.crypto.AEADBadTagException ex) {
System.out.println("tag mismatch");
}
(пример вывода) message
2) Чанковая обработка RSA: комбинированный подход
// генерируется симметричный ключ для данных большого размера
SecretKey sym = KeyGenerator.getInstance("AES").generateKey();
// симметричный ключ шифруется RSA, а данные шифруются AES
Cipher rsa = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
rsa.init(Cipher.ENCRYPT_MODE, rsaPubKey);
byte[] wrappedKey = rsa.doFinal(sym.getEncoded());
// потоковая шифровка большого файла
Cipher aes = Cipher.getInstance("AES/CBC/PKCS5Padding");
aes.init(Cipher.ENCRYPT_MODE, sym, new IvParameterSpec(iv));
try (java.io.InputStream in = new java.io.FileInputStream("large.dat");
java.io.OutputStream out = new java.io.FileOutputStream("large.dat.enc")) {
byte[] buffer = new byte[8192];
int r;
while ((r = in.read(buffer)) != -1) {
byte[] part = aes.update(buffer, 0, r);
if (part != null) out.write(part);
}
byte[] finalPart = aes.doFinal();
out.write(finalPart);
}
(пример вывода) файл large.dat.enc создан, wrappedKey хранится отдельно
3) Использование ByteBuffer для zero-copy с каналами
var channel = java.nio.channels.FileChannel.open(java.nio.file.Paths.get("in.dat"));
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
c.init(Cipher.ENCRYPT_MODE, key);
java.nio.ByteBuffer in = java.nio.ByteBuffer.allocateDirect(16384);
java.nio.ByteBuffer out = java.nio.ByteBuffer.allocateDirect(c.getOutputSize(in.capacity()));
while (channel.read(in) > 0) {
in.flip();
c.doFinal(in, out); // в реальности может потребоваться обработка по частям
out.flip();
// запись out в файл/канал
in.clear(); out.clear();
}
(пример вывода) данные зашифрованы с использованием Direct ByteBuffer
4) Обработка частичных данных с сохранением состояния
Cipher c = Cipher.getInstance("AES/CTR/NoPadding");
c.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] part1 = c.update(data1);
byte[] part2 = c.update(data2);
byte[] tail = c.doFinal();
// part1 + part2 + tail - полный результат
(пример вывода) частичные буферы объединяются в итоговый поток зашифрованных данных
5) Параллельная обработка и ограничения
Cipher экземпляры обычно не потокобезопасны. Для параллельной обработки выделяется отдельный экземпляр Cipher на каждый поток или используется пул. doFinal завершает внутреннее состояние, поэтому повторный вызов без повторной инициализации приведёт к ошибкам.