Cipher.doFinal: примеры (JAVA)

Принципы работы Cipher.doFinal в Java
Раздел: Шифрование и хеширование (Cryptography, MessageDigest)
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 и проверкой тега

Пример java
// шифрование
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: комбинированный подход

Пример java
// генерируется симметричный ключ для данных большого размера
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 с каналами

Пример java
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) Обработка частичных данных с сохранением состояния

Пример java
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 завершает внутреннее состояние, поэтому повторный вызов без повторной инициализации приведёт к ошибкам.

джава Cipher.doFinal function comments

En
Cipher.doFinal Encrypts or decrypts data in a single-part operation, or finishes a multiple-part operation