Skip to content
Kembali ke Blog
Keamanan

Dekripsi AES Gagal: Kunci, IV, Mode, dan Padding

Dekripsi AES gagal? Kunci yang salah memicu error padding, IV yang salah hanya merusak blok pertama. Debug kasus Anda gratis dengan alat online.

16 menit membaca

Dekripsi AES Gagal: Kunci, IV, Mode, dan Padding

Ketika log Anda memuat aes decryption failed, pesan yang Anda terima kemungkinan besar sedang menggambarkan masalah yang keliru. Empat bug yang sama sekali tidak berhubungan menghasilkan gejala yang nyaris identik, dan penyebab yang paling sering muncul, yaitu kunci yang salah, justru memperkenalkan diri sebagai error pengisian (padding).

Berikut urutan prioritas yang paling menghemat waktu, berangkat dari dekripsi CBC yang melempar BadPaddingException:

  1. Byte kuncinya berbeda di kedua sisi. Penyebab paling mungkin, dengan selisih yang jauh.
  2. Derivasi kuncinya berbeda: passphrase sama, KDF atau jumlah iterasi berbeda, jadi byte kuncinya pun berbeda.
  3. Ciphertext rusak di perjalanan, entah terpotong, base64-nya kacau, atau sempat bolak-balik melewati sebuah encoding teks.
  4. IV-nya salah. Nyata terjadi, tetapi ia tidak melempar error padding; ia merusak enam belas byte lalu diam saja.

Urutan itu bersifat struktural. CBC memeriksa padding sebagai langkah terakhir dekripsi, sesudah kunci bekerja dan rantainya terurai, sehingga padding jadi checksum atas segala hal di hulu dan ia gagal dengan berisik tanpa peduli bagian hulu mana yang rusak. Angka pengukurannya ada di bawah. Kalau Anda lebih suka langsung mencoba, tempelkan ciphertext Anda ke alat dekripsi AES lalu jalankan biseksi di bagian 9.

Semua angka di bawah ini kami ukur pada java 1.8.0_162, node v25.8.2, dan openssl 3.6.2. Nilai default bergeser antarversi, jadi perlakukan nomor versi itu sebagai bagian dari hasilnya.

1. Mulai dari apa yang sudah dicoret pesan error Anda

Pesan kegagalan AES nyaris tidak mengatakan apa pun tentang penyebabnya, tetapi banyak bercerita tentang apa yang tidak mungkin jadi penyebabnya. Pakai ia untuk mencoret cabang, bukan untuk memilih satu cabang.

Yang Anda lihatYang tersingkirYang masih hidup
BadPaddingException, bad decrypt, wrong final block lengthGCM; kesalahan IV murni; kegagalan decodekunci salah, KDF salah, ciphertext terpotong, byte IV termakan sebagai ciphertext, mode tidak cocok, skema padding tidak cocok
GCM Authentication failed, Unsupported state or unable to authenticate datapadding; segala teori yang melibatkan output parsialkunci salah, nonce salah, tag terpisah atau salah tempat, panjang tag salah, AAD tidak cocok
Tidak ada exception, output berupa sampahsemua mode terautentikasiECB, CTR, CBC yang sedang beruntung, mode tidak cocok, IV salah

BadPaddingException, bad decrypt, wrong final block length

Peristiwa yang sama di tiga ekosistem: Java, OpenSSL, dan .NET. Ia menyala di ujung dekripsi CBC atau ECB, saat blok plaintext terakhir tidak berakhir dengan pola PKCS#7 yang sah.

Bagian yang berguna justru sisi negatifnya: sampai sejauh ini berarti base64 atau hex Anda berhasil di-decode dan jumlah byte-nya adalah kelipatan 16 yang bukan nol, jadi lapisan transport tidak mencabik data Anda dan Anda tidak sedang berada di GCM. wrong final block length adalah pengecualiannya. Di situ jumlahnya bukan kelipatan 16, yang menunjuk ke pemotongan alih-alih ke kunci, jadi lompatlah ke bagian 8.

GCM Authentication failed dan kerabatnya

GCM membandingkan tag sebelum melepas satu byte plaintext pun, sebagaimana diwajibkan NIST SP 800-38D. Itu membuatnya jujur dengan cara yang tidak dimiliki error padding: ada sesuatu di dalam tuple (kunci, nonce, ciphertext, additional authenticated data, tag) yang tidak sama dengan yang dipakai pihak enkriptor. Ia tidak bisa memberi tahu elemen mana, dan tidak akan pernah bisa, karena mempersempit hal itu memang sengaja berada di luar tugas algoritmanya. Bagian 6 membahas elemen yang paling sering patah lintas bahasa: posisi tag, bukan nilainya.

Tidak ada error, tetapi output-nya sampah

Inilah hasil yang berbahaya, karena dasbor mencatatnya sebagai sukses. CTR tidak pernah melempar dan ECB tidak pernah melempar. CBC melempar hanya kalau pola byte terakhir gagal lolos pemeriksaan padding, dan dengan kunci yang salah byte itu praktis acak, jadi kira-kira satu dari 256 percobaan mendarat di 0x01 lalu dinyatakan sah. Sedikit di bawah 0,4% dekripsi CBC berkunci salah “berhasil”. Namun sampah punya bentuk, dan bentuk itulah yang menyebut nama bug-nya: bagian 4 dan 5 memuat dua sidik jari yang layak Anda hafalkan.

2. Error paling menyesatkan di AES

Kunci 0123456789abcdef, IV nol semua, AES/CBC/PKCS5Padding, plaintext hello world, pada java 1.8.0_162 dengan provider bawaan JDK, SunJCE. Hasilnya menyusun ulang prioritas debugging kebanyakan orang:

SkenarioPerubahanHasil pengukuran
AKunci meleset 1 byte (karakter terakhir fX)melempar javax.crypto.BadPaddingException: Given final block not properly padded. Padding-nya sendiri tidak pernah cacat; pesan error itu sepenuhnya menyesatkan
BKunci benar, IV meleset 1 bytetanpa exception, plaintext hello world kembali sebagai iello world. Hanya byte yang bersesuaian di blok pertama yang rusak
CKunci benar, dekripsi ciphertext CBC dengan AES/ECBberhasil secara senyap, tanpa exception. Ketidakcocokan mode tidak harus memunculkan apa pun

Skenario A menyesatkan orang sepanjang sore. Skenario C mengirim data rusak ke produksi.

Mengapa kunci yang salah menghasilkan error padding

Tidak ada yang keliru pada padding-nya. Pihak enkriptor menambahkan lima byte 0x05 untuk menggenapkan hello world menjadi enam belas, mengenkripsi blok itu, dan hasilnya duduk di ciphertext Anda tanpa cedera.

Kegagalannya terjadi di jalan pulang. Dekripsi CBC menjalankan block cipher secara terbalik, meng-XOR tiap hasil dengan blok ciphertext sebelumnya, dan baru setelah itu membaca ekor blok terakhir untuk menentukan berapa byte yang harus dipangkas. Dengan kunci yang salah, cipher menghasilkan enam belas byte derau, dan derau nyaris tidak pernah berakhir dengan pola PKCS#7 yang sah. Pustaka melaporkan apa yang ia lihat, padding rusak, yang memang benar sekaligus tidak berguna.

Bacalah BadPaddingException sebagai “plaintext yang saya rekonstruksi tidak berakhir sebagaimana plaintext ber-padding berakhir”. Alasan paling mungkin rekonstruksi Anda meleset adalah kunci, dan itulah sebabnya pencarian aes decrypt wrong key dan pencarian bad padding exception mendaratkan Anda di utas yang sama: dua gejala itu sebenarnya satu gejala. Satu catatan desain yang menempel di situ: jangan pernah membuka perbedaan itu kepada pemanggil, karena kemampuan membedakan “padding tidak sah” dari “padding sah, isinya salah” itulah santapan serangan padding oracle (Vaudenay, EUROCRYPT 2002).

Apa yang GCM lakukan secara berbeda

GCM membalik urutannya, memverifikasi tag sebelum menghasilkan plaintext apa pun, sehingga tidak ada celah di mana byte yang setengah benar sempat ada. Kegagalan GCM tidak pernah meninggalkan Anda dalam keraguan apakah output-nya nyata, sebab memang tidak ada output. GCM juga sama sekali tidak punya padding, karena di bawahnya ia adalah counter mode, jadi panjang ciphertext sama dengan panjang plaintext. Karena itu, error padding pada sistem yang Anda kira GCM justru membuktikan sistem itu bukan GCM, biasanya karena konfigurasi yang jatuh kembali ke CBC.

3. Apakah kedua sisi memakai byte kunci yang sama?

AES tidak melihat string kunci Anda. Ia melihat 16, 24, atau 32 byte. Dua sistem bisa memegang bahan kunci yang identik di file konfigurasi dan tetap berselisih, karena “identik” itu sifat teksnya, bukan sifat byte-nya.

Tiga cara sebuah string kunci berubah menjadi byte

Serahkan string harfiah 0123456789abcdef ke tiga pustaka berbeda:

sebagai hex           -> 8 byte     (panjang kunci AES tidak sah)
sebagai base64        -> 12 byte    (panjang kunci AES tidak sah)
sebagai UTF-8 mentah  -> 16 byte    (AES-128 sah)

Enam belas karakter, tiga jumlah byte. Kasus ini menjengkelkan justru karena ia sah di ketiga pembacaan: setiap karakternya ada di alfabet hex maupun base64, dan enam belas karakter adalah panjang yang legal bagi kedua decoder, jadi tidak ada yang error saat parsing.

Panduan troubleshooting JWT invalid signature memuat matriks lintas-pustaka lengkap tentang bagaimana tiap ekosistem menafsirkan sebuah string secret; versi singkat untuk AES adalah catat encoding bahan kunci Anda lalu buat kedua sisi men-decode secara eksplisit. Bentuk HMAC dari bug yang sama menggigit para penerima webhook, dan itu dibahas di panduan verifikasi tanda tangan webhook.

AES itu ketat: tepat 16, 24, atau 32 byte

Di sinilah AES berbeda dari primitif yang biasanya lebih dulu ditemui developer. HMAC menerima panjang kunci berapa pun: RFC 2104 meng-hash apa saja yang lebih panjang dari ukuran blok dan menambal nol pada apa saja yang lebih pendek, jadi sebuah generator HMAC menerima secret 7 byte maupun 700 byte tanpa mengeluh. AES punya tepat tiga panjang kunci yang legal dan menolak semua sisanya sebelum satu blok pun diproses.

Keketatan itu sebenarnya hadiah, karena kesalahan panjang adalah satu-satunya kegagalan AES yang menyebut penyebabnya sendiri alih-alih bersembunyi di balik padding. Alat kami merumuskannya sebagai Key must be 16, 24, or 32 bytes (AES-128/192/256). Jebakan yang menghasilkan panjang keliru:

  • Baris baru di ekor dari KEY=$(cat key.txt) atau echo "$KEY". Pakai printf dan echo -n. Satu spasi di ekor yang ikut tersalin dari UI secrets manager punya efek yang persis sama.
  • Prefiks 0x yang tersalin dari debugger: tiga puluh empat karakter yang sudah bukan hex yang sah lagi.
  • Karakter non-ASCII. contraseña itu 10 karakter dan 11 byte dalam UTF-8, jadi passphrase “32 karakter” dengan satu huruf beraksen sebenarnya 33 byte.

SecretKeySpec dan charset default platform

Java punya versi masalah ini yang baru muncul setelah deployment. "my secret".getBytes() tanpa argumen memakai charset default platform, yang sebelum JDK 18 berasal dari properti file.encoding dan karenanya dari OS serta locale mesin itu. Laptop ber-UTF-8 dan container ber-ANSI_X3.4-1968 menghasilkan byte yang berbeda untuk karakter non-ASCII mana pun. JEP 400 menjadikan UTF-8 sebagai default di JDK 18, yang memperbaiki kode baru dan tidak memperbaiki apa pun selain itu.

// salah: byte-nya bergantung pada mesin
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(), "AES");

// benar: byte-nya tidak bergantung pada apa pun
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "AES");

Kalau kode Anda jalan di lokal, gagal di server dengan error padding, dan passphrase-nya memuat sesuatu di luar ASCII, periksa hal ini lebih dulu.

4. IV: di mana tempatnya dan seperti apa rupa IV yang salah

aes iv mismatch adalah kegagalan yang paling awal dicurigai orang dan paling akhir terdiagnosis, karena perilakunya tidak seperti yang lain. Ia senyap dan ia lokal.

IV yang salah merusak tepat satu blok

Lihat lagi skenario B. Kunci benar, IV meleset satu byte:

hello world   ->   iello world

Tanpa exception, satu karakter. Tuliskan langkah CBC untuk blok pertama dan semuanya jadi jelas: P1 = D(C1) XOR IV. IV di-XOR langsung ke blok plaintext pertama dan tidak menyentuh apa pun selain itu, jadi membalik satu bit IV berarti membalik bit yang sama pada plaintext di posisi yang sama. Di sini h (0x68) menjadi i (0x69), jadi byte pertama IV bergeser tepat sebesar 0x01.

Sidik jarinya gampang dibaca. Di CBC, 16 byte pertama berupa sampah sementara semua sesudahnya bersih berarti IV-nya salah dan kuncinya benar. Setiap blok berupa sampah berarti kuncinya yang salah. Satu pengamatan itu memisahkan dua penyebab tersering tanpa mengubah satu baris kode pun, dan alat dekripsi AES menampilkan byte hasil decode sehingga Anda bisa membacanya langsung.

Soal mengapa tidak ada yang melempar: hello world panjangnya 11 byte, jadi ia satu blok saja, dan padding PKCS#7-nya bermukim di byte 11 sampai 15 blok itu. Byte IV yang berubah adalah byte 0, jadi wilayah padding tidak tersentuh dan lolos validasi. Rusakkan byte IV di posisi 11 atau sesudahnya, dan yang Anda dapat justru error padding, satu jalur lagi yang membuat error padding berbohong kepada Anda.

Tiga konvensi pengiriman

Tidak ada standar soal ke mana IV pergi, hanya tiga kebiasaan yang saling berinteroperasi dengan buruk.

Ditempel di depan: iv || ciphertext, konvensi paling umum sekaligus default di alat kami. Kedua sisi harus sepakat berapa banyak yang dipangkas: 16 byte untuk CBC dan CTR, 12 untuk GCM. Bug cerminnya adalah produsen yang menempelkan IV di depan sementara konsumen tidak memangkasnya. Akibatnya 16 byte pertama “ciphertext” sebenarnya IV, setiap blok bergeser, dan Anda dapat error padding.

Field terpisah: {"iv": "...", "ciphertext": "..."} secara prinsip lebih rapi, tetapi ia melipatgandakan tempat di mana encoding bisa berselisih, sebab kini IV punya pertanyaan base64-lawan-hex-nya sendiri.

Konstanta tetap, biasanya nol semua, ditulis mati karena seseorang butuh determinisme. Ia berinteroperasi dengan sempurna, dan justru itu yang membuatnya berbahaya: di CBC, IV tetap membocorkan kesamaan antarrekaman, dan di GCM, memakai ulang nonce di bawah satu kunci mengungkap XOR dari kedua plaintext serta bisa membuka subkunci GHASH yang mengautentikasi tag. SP 800-38D bicara tegas soal keunikan.

Sakelar ciphertext telanjang di alat kami plus penimpaan IV secara eksplisit menguji ketiga konvensi itu terhadap byte yang sama dalam semenit.

IV milik GCM 12 byte, bukan 16

Tim yang mengadopsi GCM dengan menyunting jalur CBC yang sudah ada cenderung membawa serta IV 16 byte, dan hasilnya gagal tanpa petunjuk sama sekali.

SP 800-38D membakukan IV 96 bit. Panjang lain diizinkan, tetapi itu bukan sekadar “IV yang lebih panjang”: ketika IV bukan 96 bit, GCM menurunkan blok counter awalnya dengan menjalankan IV melalui GHASH alih-alih memakainya langsung. Karena itu, 16 byte yang sama saat dipakai sebagai nonce menghasilkan keystream dan tag yang sepenuhnya berbeda dari yang dihasilkan 12 byte pertamanya, dan Anda dapat kegagalan autentikasi generik. Kalau ciphertext-nya datang dari tempat lain dan Anda sedang menebak tata letaknya, hitung mundur: tag adalah 16 byte terakhir, nonce hampir selalu 12 byte pertama.

5. Ketidakcocokan mode, termasuk yang senyap

Cipher.getInstance("AES") itu ECB

Java membolehkan Anda menyebut sebuah cipher tanpa menyebut mode maupun skema padding-nya. Ia tidak menolak dan ia tidak memperingatkan. Di bawah provider bawaan JDK, SunJCE, ia mengisi bagian kosongnya dengan ECB dan PKCS5Padding.

Membuktikannya butuh eksperimen yang tepat: enkripsi 32 byte identik (dua blok berisi A) dengan kunci 0123456789abcdef, lalu periksa apakah kedua blok ciphertext-nya sama. Pada java 1.8.0_162:

getInstance("AES")           ciphertext = 3bfd04cc0d7ed55358e2cbe19de213833bfd04cc0d7ed55358e2cbe19de21383377222e061a924c591cd9c27ea163ed4
  block1 = 3bfd04cc0d7ed55358e2cbe19de21383
  block2 = 3bfd04cc0d7ed55358e2cbe19de21383   <- blok identik = sidik jari ECB (struktur plaintext bocor)
getInstance("AES/CBC/PKCS5Padding")  blok berbeda = chaining aktif

Identik byte demi byte. Itulah tanda tangan ECB, sifat yang sama yang membuat gambar penguin terenkripsi yang terkenal itu tetap terlihat seperti penguin. Eksperimen ini hanya bekerja dengan blok plaintext yang identik: enam belas byte A diikuti enam belas byte B juga menghasilkan dua blok ciphertext berbeda di bawah ECB, dan Anda akan keliru menyimpulkan default-nya CBC.

Batasi cakupan hasilnya dengan cermat: ia menggambarkan provider bawaan JDK, SunJCE, pada versi di atas. Transformasi default adalah keputusan provider, jadi provider pihak ketiga seperti BouncyCastle bisa menerjemahkan singkatan yang sama secara berbeda. Generalisasinya bukan “Java berarti ECB” melainkan “string transformasi tanpa kualifikasi berarti apa pun yang provider Anda putuskan, dan karena itulah Anda tidak pernah menulisnya”.

Mode yang salah belum tentu memunculkan error

Skenario C mendekripsi ciphertext CBC dengan AES/ECB dan mengembalikan plaintext yang benar tanpa exception. Itu terdengar mustahil sampai Anda menuliskan aritmetikanya. Enkripsi CBC untuk blok pertama adalah C1 = E(P1 XOR IV), dan dekripsi ECB atas blok itu adalah D(C1) = P1 XOR IV. IV di sini nol semua, jadi P1 XOR 0 = P1 dan blok pertama terdekripsi dengan sempurna. hello world panjangnya satu blok, jadi “blok pertama” adalah seluruh pesannya.

Aturan umumnya layak Anda bawa: dengan IV nol, ECB dan CBC sepakat pada blok pertama dan berselisih pada setiap blok sesudahnya. Dekripsi pesan CBC yang panjang sebagai ECB dan Anda dapat enam belas byte bersih diikuti derau, kebalikan persis dari sidik jari IV-salah. Dua bentuk yang berlawanan, dua bug yang berbeda, tanpa pesan error pada keduanya. IV nol yang ditulis mati cukup sering dipakai sehingga ini bukan sekadar keingintahuan laboratorium.

Apa yang Anda dapat dari pemanggilan minimal di tiap bahasa

EkosistemPemanggilan minimalMode yang benar-benar Anda dapat
Java (SunJCE)Cipher.getInstance("AES")ECB dengan PKCS5Padding, secara senyap
Node cryptocreateDecipheriv('aes-256-cbc', key, iv)apa pun yang disebut string algoritmanya; tidak ada default
Web Cryptocrypto.subtle.decrypt({ name: 'AES-CBC', iv }, ...)disebut eksplisit; ECB sama sekali tidak diimplementasikan
Python cryptographyCipher(algorithms.AES(key), modes.CBC(iv))objek mode-nya wajib
PyCryptodomeAES.new(key, AES.MODE_ECB)argumen wajib, tetapi ECB ada di situ, di daftar autocomplete
Go crypto/aesaes.NewCipher(key) mengembalikan cipher.Block mentahmemanggil Decrypt pada blok itu sama dengan ECB; bungkus dengan cipher.NewCBCDecrypter atau cipher.NewGCM
CryptoJSCryptoJS.AES.decrypt(ct, "passphrase")CBC, PKCS#7, EVP_BytesToKey dengan MD5 (lihat bagian 7)

Ekosistem yang menaruh mode di dalam string atau objek tidak pernah mengejutkan Anda. Dua yang menawarkan pemanggilan “AES saja”, yaitu Java dan Go, adalah asal muasal laporan ECB-tak-sengaja. Kalau ciphertext Anda datang tanpa dokumentasi, uji ulang byte yang sama sambil berganti mode sebelum menyentuh kuncinya.

6. GCM: byte yang sama, API yang berbeda

Sebagian besar kegagalan aes gcm auth tag lintas bahasa sama sekali bukan soal kriptografi. Kedua sisi menghitung 16 byte yang sama dan berselisih soal di mana byte itu tinggal.

Hasil pengukuran

Kunci = 32 byte 0123456789abcdef0123456789abcdef, IV = 12 byte nol, plaintext hello world, pada node v25.8.2 dan java 1.8.0_162:

Node   ciphertext = a616cd6d7d2328379d41e5                    (11 B)   <- update+final
       authTag    = c87af9f8ad7148e873fa797292c0af3f          (16 B)   <- diambil terpisah lewat getAuthTag()
Java   doFinal()  = a616cd6d7d2328379d41e5c87af9f8ad7148e873fa797292c0af3f   (27 B)   <- ciphertext dan tag sudah tergabung

Node ciphertext || authTag sama persis dengan Java doFinal(), seluruh 27 byte-nya. Tidak ada beda encoding, tidak ada yang perlu dinegosiasikan: Node menyerahkan dua potongan itu secara terpisah dan Java menyerahkannya sudah terlem jadi satu. Panjangnya juga cocok: 11 byte plaintext menghasilkan 11 byte ciphertext, karena GCM tidak menambahkan padding. Dari situ jelas error padding tidak mungkin datang dari jalur GCM yang asli.

Digabung atau dipisah, menurut runtime

RuntimeAPI enkripsiKe mana tag-nya berakhir
Node cryptoupdate() + final(), lalu getAuthTag()terpisah
Java (SunJCE, AES/GCM/NoPadding)doFinal()ditempel di belakang
Go cipher.AEADSeal()ditempel di belakang
Python cryptography, AESGCMencrypt()ditempel di belakang
Python cryptography, Cipher + modes.GCMfinalize(), lalu encryptor.tagterpisah
Web Cryptocrypto.subtle.encryptditempel di belakang

Node adalah yang menyimpang sendiri di antara API tingkat tinggi, dan karena itu arah Node-ke-apa-pun paling sering muncul di laporan kegagalan. Kalau tabel di atas belum menyelesaikan perkaranya, cocokkan posisi tag pada byte Anda sendiri lebih dulu. Mengemas output Node untuk konsumen Java, Go, Python, atau browser:

const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const ct = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);
const packed = Buffer.concat([ct, cipher.getAuthTag()]);   // sekarang cocok dengan doFinal()

Membongkar gumpalan tergabung untuk Node:

const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(packed.subarray(packed.length - 16));  // harus datang sebelum final()
const pt = Buffer.concat([
  decipher.update(packed.subarray(0, packed.length - 16)),
  decipher.final(),
]);

Batasan urutan itu nyata: panggil setAuthTag() sesudah final() dan Node melempar Unsupported state or unable to authenticate data bahkan ketika setiap byte-nya sudah benar.

Panjang tag bervariasi, satuannya tidak

GCM mengizinkan tag sepanjang 128, 120, 112, 104, atau 96 bit, dengan 64 dan 32 dicadangkan untuk aplikasi dengan sumber daya terbatas (SP 800-38D, Lampiran C). Hampir semua orang memakai 128, dan repotnya ada pada satuan yang dituntut tiap API. Java menghitung dalam bit: new GCMParameterSpec(128, iv). Web Crypto juga bit, lewat { name: 'AES-GCM', iv, tagLength: 128 }, dengan 128 sebagai default. Node menghitung dalam byte: createCipheriv(algo, key, iv, { authTagLength: 16 }).

new GCMParameterSpec(16, iv) adalah baris Java yang terlihat legal padahal meminta tag 16 bit; sebagian JDK menolaknya, dan di tempat ia diterima Anda baru saja menukar jaminan integritas dengan lemparan koin satu banding 65.536. Ketika kedua sisi berselisih soal panjang tag, panjang paket hasilnya ikut berbeda, sehingga penerima mengiris di batas yang salah dan mendapat kegagalan autentikasi yang sama sekali tidak berhubungan dengan kunci.

7. Yang Anda punya passphrase, bukan kunci

Kalau salah satu sisi menerima string yang diketik manusia, ada fungsi derivasi kunci di antara string itu dan AES, dan ketidakcocokan KDF sifatnya tak terlihat. Ia tidak pernah error. Ia mengembalikan 32 byte yang sempurna, yang kebetulan 32 byte yang salah, dan kegagalannya muncul satu lapis di bawah sebagai (Anda sudah tahu yang ini) error padding.

PBKDF2 butuh empat hal yang sejajar

  • Salt. Dalam format Salted__ milik OpenSSL ia 8 byte di dalam ciphertext; dalam format passphrase di alat kami ia prefiks 16 byte; dalam skema rakitan sendiri ia kerap berupa konstanta yang ditulis mati.
  • Iterasi. openssl enc -pbkdf2 default-nya 10.000. OWASP saat ini merekomendasikan 600.000 untuk PBKDF2-HMAC-SHA256, dan itulah angka yang mode passphrase kami pakai. Setiap framework memilih angkanya sendiri.
  • Hash. SHA-1 lawan SHA-256 lawan SHA-512. Kode lama dan sebagian SDK mobile masih memakai SHA-1 sebagai default.
  • Panjang output. Tiga puluh dua byte untuk AES-256, enam belas untuk AES-128. Sebagian skema menurunkan kunci dan IV sekaligus dari satu pemanggilan yang lebih panjang, dan itu tidak akan pernah cocok dengan derivasi 32 byte biasa.

EVP_BytesToKey, dan mengapa CryptoJS terus-terusan tidak jalan

cryptojs aes decrypt not working biasanya satu ketidakcocokan yang sangat spesifik. CryptoJS.AES.encrypt(text, "passphrase") tidak memakai PBKDF2. Ia memakai EVP_BytesToKey, derivasi OpenSSL pra-1.1, dengan MD5 dan satu iterasi saja.

Ada satu hal yang EVP_BytesToKey kerjakan dan PBKDF2 tidak: ia menurunkan kunci dan IV dari passphrase serta salt dalam satu jalan. Karena itu file Salted__ OpenSSL tidak membawa field IV terpisah, dan karena itu pula mereproduksi output CryptoJS dengan PBKDF2 plus IV acak keliru di dua tempat sekaligus.

Formatnya ketahuan sekali lihat: 8 byte ASCII Salted__ diikuti salt 8 byte, di-encode base64, selalu diawali U2FsdGVkX1. Kalau ciphertext Anda diawali begitu, ia berasal dari passphrase dan Anda perlu tahu derivasi mana yang dipakai; alat dekripsi AES mendeteksi prefiks itu dan berpindah di antara ketiganya tanpa perubahan kode.

Mengapa password yang sama memberi kunci yang berbeda

Tidak ada yang namanya “password AES”. Setiap pustaka menciptakan jalurnya sendiri dari string menjadi kunci:

PenghasilDerivasiHasil untuk satu passphrase
CryptoJS AES.encrypt(text, pass)EVP_BytesToKey, MD5, 1 iterasikunci A
openssl enc 1.0.2 dan sebelumnyaEVP_BytesToKey, MD5, 1 iterasikunci A
openssl enc 1.1+ tanpa -pbkdf2EVP_BytesToKey, SHA-256, 1 iterasikunci B
openssl enc -pbkdf2PBKDF2-HMAC-SHA256, 10.000 iterasikunci C
Mode passphrase kamiPBKDF2-HMAC-SHA256, 600.000 iterasikunci D
Java, Python, Gotidak ada default sama sekali; Anda yang menulis derivasinyaapa pun yang Anda tulis

Empat kunci dari satu password sebelum siapa pun berbuat salah. Peningkatan dari 1.0.2 ke 1.1 mengubah digest default dari MD5 ke SHA-256, dan di situlah perintah yang sama di mesin yang lebih baru berhenti bisa membuka ciphertext dari skrip lama. Kalau Anda mewarisi data dan tidak ada yang ingat toolchain-nya, coba derivasinya sesuai urutan itu. Ini uji tiga arah, bukan pencarian.

8. Apa yang dilakukan lapisan transport terhadap byte Anda

Ciphertext adalah biner acak yang seragam, dan itu membuatnya semaksimal mungkin bermusuhan dengan apa pun yang memperlakukan byte sebagai teks. Sebagian besar kegagalan AES sama sekali tidak melibatkan cipher-nya. Di Indonesia, bentuk kegagalan ini paling sering muncul di jalur callback gerbang pembayaran seperti Midtrans, Xendit, atau DOKU. Begitu ada payload yang lewat AES di situ, mode dan padding di kedua sisi tetap harus sejajar, dan urutan diagnosis di halaman ini berlaku apa adanya.

Varian Base64 dan padding yang hilang

Base64 standar (RFC 4648 §4) memakai + dan /; varian URL-safe (§5) memakai - dan _. String URL-safe yang diserahkan ke decoder standar entah melempar error atau, pada decoder yang longgar, membuang karakter bermasalah itu secara senyap lalu mengembalikan byte yang pendek dan salah lurus, dan itulah alasan Java menyediakan Base64.getUrlDecoder() dan Base64.getDecoder() sebagai dua objek terpisah. Sebagian encoder juga membuang = di ekor, sebagian decoder justru mewajibkannya, dan jalur kode yang bersinggungan dengan JWT membuangnya secara default.

Sebelum mencurigai kunci, decode ciphertext-nya lalu cocokkan panjangnya dengan mode:

  • CBC dan ECB: kelipatan 16 yang bukan nol. Angka lain berarti pemotongan atau masalah decode, bukan masalah kunci.
  • GCM: panjang ciphertext sama dengan panjang plaintext, ditambah 16 untuk tag, ditambah 12 di depan kalau nonce-nya ditempel di depan.
  • CTR: panjang berapa pun, jadi pemeriksaan ini tidak memberi tahu apa-apa.

Decoder Base64 memberi Anda jumlah byte hanya dengan sekali tempel, sering kali pengukuran tercepat di sepanjang penyelidikan.

Baris baru, tanda kutip pintar, dan perjalanan bolak-balik UTF-8

openssl base64 membungkus output pada kolom ke-64 kecuali Anda memberi -A, dan sebagian decoder melewati baris baru yang tersisip sementara sebagian lain menolaknya, jadi file yang sama ter-decode di satu mesin dan gagal di mesin lain. Menyalin lewat aplikasi chat atau editor dokumen mengubah tanda kutip lurus jadi melengkung dan tanda hubung jadi en dash, dan bedanya nyaris tak terlihat di terminal.

Yang tidak bisa dipulihkan adalah perjalanan bolak-balik UTF-8. Kalau output AES mentah pernah ditampung sebagai string tanpa di-encode lebih dulu (new String(cipherBytes) di Java, bytes.decode('utf-8', errors='replace') di Python, sebuah TextDecoder di mana pun), setiap urutan byte yang bukan UTF-8 sah runtuh menjadi U+FFFD, dan meng-encode-nya kembali memberi Anda EF BF BD di tempat data Anda dulu berada. Karena kira-kira separuh byte acak itu non-ASCII, sebagian besar ciphertext-nya hancur dan tidak ada kunci yang bisa memulihkannya; panduan encoding UTF-8 dan UTF-16 menjelaskan mengapa kerugian ini searah. Ciphertext biner berjalan sebagai base64, sebagai hex, atau sebagai biner, dan tidak pernah sebagai string.

Kolom database

Penyimpanan menimbulkan kerusakan yang sama, hanya lebih diam-diam. Ciphertext yang ditulis ke VARCHAR(255) dan kelebihan satu blok akan terpotong, dan MySQL di luar strict mode melakukannya tanpa error. Ekor itulah tempat blok padding dan tag GCM tinggal, jadi baris yang “berhasil” ditulis berbulan-bulan lalu kini gagal, dan kalau potongannya jatuh tepat di batas 16 byte, pemeriksaan panjang di atas pun tidak akan menangkapnya. Konversi charset menuntaskan sisanya: kolom latin1 yang menerima byte UTF-8 menulis ulang data Anda saat masuk.

Simpan ciphertext di VARBINARY, BLOB, atau bytea, atau simpan base64 di kolom teks yang lapang.

9. Alur biseksi yang menemukannya dalam lima menit

Setiap bagian di atas mempersempit satu variabel. Menjalankannya secara berurutan terhadap implementasi rujukan yang Anda kendalikan akan cepat mengerucut, dan alat berbasis browser cocok jadi rujukan itu karena Anda bisa mengubah satu setelan pada satu waktu lalu melihat byte-nya, tanpa satu byte pun meninggalkan halaman: dekripsinya berjalan lokal di browser Anda, jadi kunci dan ciphertext tidak pernah sampai ke server kami.

  1. Langkah 0: ukur bentuknya. Decode ciphertext-nya lalu catat jumlah byte, beberapa byte pertama, dan apakah ia diawali U2FsdGVkX1. Cocokkan jumlahnya dengan bagian 8. Kalau ia bukan kelipatan 16 sementara Anda yakin sedang di CBC, berhenti: ini bug transport.

  2. Langkah 1: enkripsi plaintext yang Anda ketahui. Di alat enkripsi AES, enkripsi sebuah string pendek yang Anda ketahui dengan parameter yang Anda yakini dipakai produksi, lalu bandingkan bentuk kedua output itu alih-alih nilainya: panjang total, byte prefiks, ada tidaknya header salt. Ketidakcocokan berarti asumsi Anda tentang format atau KDF-nya keliru, dan sebanyak apa pun Anda mengutak-atik kunci tidak akan menolong.

  3. Langkah 2: putar derivasinya. Untuk data yang berasal dari passphrase, jalankan PBKDF2 dengan jumlah iterasi yang persis, lalu EVP-SHA256, lalu EVP-MD5 di alat dekripsi AES. Tepat satu di antaranya bisa benar. Kalau tidak ada yang berhasil, bug-nya ada di atas KDF.

  4. Langkah 3: singkirkan semua konvensi. Beralih ke kunci mentah, nyalakan ciphertext telanjang, sodorkan IV secara eksplisit. Sekarang Anda menyatakan dengan tepat byte mana yang kunci, IV, dan ciphertext, tanpa ada yang disimpulkan sendiri. Kalau di sini ia terdekripsi tetapi di kode Anda tidak, bug Anda adalah bug pembingkaian (prefiks IV yang tidak dipangkas, tag di tempat yang salah), bukan bug kriptografi.

  5. Langkah 4: balik mode-nya. Coba CBC, lalu CTR, lalu GCM terhadap byte yang sama. CTR yang mengembalikan teks terbaca di tempat CBC gagal berarti mode-nya yang tidak cocok, dan Anda tidak perlu menyentuh kunci sama sekali.

  6. Langkah 5: baca sampahnya. Blok pertama rusak dan sisanya bersih berarti IV. Blok pertama bersih dan sisanya rusak berarti Anda mendekripsi CBC sebagai ECB dengan IV nol. Semuanya rusak berarti kunci atau derivasinya.

10. FAQ

Mengapa kode AES saya jalan di lokal tetapi gagal di produksi?

Lingkungan produksi mengubah sesuatu yang tidak ada di source control, dan di lokal hal itu kebetulan sudah benar. Tersangka biasa, berurutan: kunci datang dari environment variable atau secret manager dengan baris baru di ekor; charset default platform Java berbeda antara laptop dan container, sehingga getBytes() menghasilkan byte yang berbeda (bagian 3); OpenSSL di produksi 1.1+ sementara skrip lokal Anda menyasar 1.0.2, sehingga digest EVP_BytesToKey berubah dari MD5 ke SHA-256; atau kolom database memotong ciphertext hanya di satu lingkungan. Cetak dulu panjang kunci dan panjang ciphertext dalam byte di kedua sisi, karena dua angka itu biasanya sudah menyelesaikan perkaranya.

Saya enkripsi di Node dan tidak bisa dekripsi di Java. Mulai dari mana?

Mulai dari tag GCM, penyebab paling umum sekaligus paling tidak kentara pada arah Node ke Java. Node mengembalikan ciphertext dan tag secara terpisah; doFinal() di Java mengharapkan keduanya tergabung sebagai ciphertext || tag, dan bagian 6 menunjukkan bahwa selain itu byte-nya identik. Kalau Anda memakai CBC, mulailah dari konvensi IV: apakah Node menempelkannya di depan, dan apakah sisi Java memangkas 16 byte sebelum mendekripsi? Ketiga barulah kuncinya sendiri, di mana Buffer.from(k, 'hex') dan k.getBytes(StandardCharsets.UTF_8) menghasilkan panjang berbeda dari string yang sama.

Apakah PKCS5Padding di Java sama dengan PKCS#7?

Untuk AES, ya: PKCS5Padding di Java secara efektif adalah PKCS#7. PKCS#5 (RFC 8018) hanya didefinisikan untuk blok 8 byte; PKCS#7 (RFC 5652) menggeneralisasi skema itu ke ukuran blok 1 sampai 255 byte. PKCS5Padding di Java yang diterapkan pada block cipher 16 byte mengimplementasikan perilaku PKCS#7, dan namanya sekadar peninggalan sejarah, jadi ini tidak akan pernah jadi bug Anda. NoPadding yang bisa jadi bug: ia menuntut plaintext yang sudah kelipatan 16, dan saat dekripsi ia menyerahkan padding kembali sebagai data, sehingga Anda melihat teks yang masuk akal dengan byte ekor semacam \x05\x05\x05\x05\x05.

Kunci saya 32 karakter, tetapi AES bilang panjang kuncinya tidak sah. Kenapa?

Kesalahan panjang berarti pustaka menerima jumlah byte yang bukan 16, 24, atau 32. Dengan string 32 karakter, biasanya itu baris baru di ekor (33 byte), prefiks 0x yang membuat string-nya bukan hex sah, atau karakter non-ASCII yang memakan dua atau tiga byte dalam UTF-8. Varian yang lebih berbahaya justru ketika Anda tidak mendapat error sama sekali: 32 karakter hex ter-decode jadi 16 byte sah dan 32 karakter base64 ter-decode jadi 24 byte sah, keduanya panjang AES yang legal. Pustaka menerimanya, memakai kunci yang salah, lalu menyodorkan kegagalan padding kepada Anda. Periksa jumlah byte, bukan jumlah karakter.

Dekripsi “berhasil” tetapi output-nya sampah. Apa yang salah?

Dekripsi bisa “berhasil” sambil mengembalikan sampah karena Anda sedang berada di mode yang tidak memverifikasi apa pun. CTR dan ECB tidak pernah melempar, dan CBC melempar hanya ketika pola byte terakhir gagal lolos pemeriksaan padding, yang bisa dilewati kunci salah sedikit di bawah 0,4% dari waktu. Baca bentuknya: 16 byte pertama rusak dan sisanya bersih berarti IV; 16 byte pertama bersih dan sisanya rusak berarti Anda mendekripsi ciphertext CBC sebagai ECB dengan IV nol; rusak merata berarti kunci atau derivasinya. Teks terbaca dengan beberapa byte ekor yang aneh berarti NoPadding pada data yang ber-padding. Perbaikan jangka panjangnya adalah GCM, supaya kata “berhasil” punya arti.

Masih bisakah saya mendekripsi kalau IV-nya hilang?

Tanpa IV Anda masih bisa mendekripsi semuanya di CBC kecuali 16 byte pertama. Blok 2 dan seterusnya dipulihkan sebagai D(C_i) XOR C_{i-1}, dan semua input untuk itu sudah ada di dalam ciphertext, jadi hanya blok pertama yang butuh IV. Kalau Anda juga tahu bagaimana plaintext-nya dimulai, misalnya gumpalan JSON yang diawali {"userId":, Anda bisa memulihkan IV-nya secara langsung sebagai D(C1) XOR P1. Di CTR, IV menyemai seluruh keystream, jadi kehilangan IV berarti kehilangan segalanya. Di GCM, nonce menyuapi counter sekaligus tag, jadi tidak ada pemulihan sebagian.

Bisakah saya memulihkan plaintext kalau tag GCM terpotong atau hilang?

Dengan tag GCM yang terpotong atau hilang, secara matematis masih bisa, secara praktis butuh usaha. Di bawahnya GCM adalah mode CTR, jadi kunci dan nonce saja sudah cukup untuk mereproduksi keystream. Tidak ada pustaka arus utama yang mau melakukannya untuk Anda: Java, Go, Python, dan Web Crypto semuanya menolak melepas plaintext tanpa tag yang sah, dan itu memang disengaja. Jalan memutarnya adalah mendekripsi byte yang sama sebagai AES-CTR dengan blok counter awal diisi nonce 12 byte diikuti 00000002, karena di situlah blok data pertama GCM dimulai. Anda mendapatkan datanya kembali dan menyerahkan seluruh jaminan integritas, jadi perlakukan hasilnya sebagai tidak tepercaya. Kalau 16 byte tag Anda masih utuh dan autentikasinya tetap gagal, tag-nya tidak hilang dan yang jadi bug Anda adalah sesuatu yang lain di halaman ini. Bawa ke alat dekripsi AES lalu mulai dari langkah 0.

Tag: aes encryption debugging cryptography interoperability

Artikel Terkait

Lihat semua artikel