AES şifre çözme hatası: anahtar, IV, mod ve dolgu
Loglarınızda AES şifre çözme hatası göründüğünde aldığınız mesaj genelde yanlış problemi anlatır. Birbiriyle ilgisiz dört hata neredeyse aynı belirtileri üretir ve en yaygın olanı, yani yanlış anahtar, kendini bir dolgu hatası olarak duyurur.
BadPaddingException fırlatan bir CBC şifre çözmesinden yola çıkarsanız, en çok zaman kazandıran sıralama şudur:
- İki tarafın anahtar baytları farklı. Açık ara en olası neden.
- Anahtar türetme farklı: aynı parola, farklı KDF ya da farklı yineleme sayısı, dolayısıyla farklı anahtar baytları.
- Şifreli metin yolda bozuldu; kırpıldı, Base64 katmanında bozuldu ya da bir metin kodlamasından gidip geldi.
- IV yanlış. Gerçek bir neden ama dolgu hatası fırlatmaz. On altı baytı bozar ve sessiz kalır.
Bu sıralama yapısaldır. CBC dolguyu en sona bırakır: önce anahtar uygulanır, sonra zincir çözülür, sıra ancak ondan sonra dolguya gelir. Dolgu böylece kendisinden önceki her şeyin üzerinde bir sağlama toplamına dönüşür ve yukarıda ne bozulursa bozulsun yüksek sesle başarısız olur. Aceleniz varsa şifreli metninizi AES şifre çözme aracına yapıştırıp 9. bölümdeki ikiye bölme akışını uygulayın.
Aşağıdaki her şey java 1.8.0_162, node v25.8.2 ve openssl 3.6.2 üzerinde ölçüldü. Varsayılanlar sürümden sürüme değişir, o yüzden sürüm numaralarını sonucun bir parçası olarak görün.
1. Hatanızın gerçekte neyi elediğiyle başlayın
Bir AES hata mesajı nedeni hakkında neredeyse hiçbir şey, nedenin ne olamayacağı hakkında ise çok şey söyler. Onu bir dal seçmek için değil, dalları elemek için kullanın. Türkçe kaynaklarda bu işlemin tek bir adı yok: şifre çözme, deşifre etme ve doğrudan decrypt aynı şeyi anlatır; dolgu ile padding de birbirinin yerine geçer. Hatanızı Türkçe ararken sonuç çıkmıyorsa aynı sorguyu bu karşılıklarla tekrarlayın.
| Gördüğünüz | Elediği | Hâlâ geçerli olan |
|---|---|---|
BadPaddingException, bad decrypt, wrong final block length | GCM; salt bir IV hatası; çözme arızası | yanlış anahtar, yanlış KDF, kırpılmış şifreli metin, şifreli metin sanılıp yutulan IV baytları, mod uyuşmazlığı, dolgu şeması uyuşmazlığı |
GCM Authentication failed, Unsupported state or unable to authenticate data | dolgu; kısmi çıktı içeren her teori | yanlış anahtar, yanlış nonce, ayrılmış ya da yanlış yere konmuş etiket, yanlış etiket uzunluğu, uyuşmayan AAD |
| İstisna yok, çıktı bozuk | kimlik doğrulamalı her mod | ECB, CTR, şansı yaver giden CBC, mod uyuşmazlığı, yanlış IV |
BadPaddingException, bad decrypt, wrong final block length
Üç ekosistemde aynı olay: Java, OpenSSL ve .NET. CBC ya da ECB şifre çözmesinin sonunda, son düz metin bloğu geçerli bir PKCS#7 kalıbıyla bitmediğinde tetiklenir.
İşe yarayan kısmı olumsuz olanı: buraya kadar gelebilmiş olmanız, Base64 ya da hex çözümünüzün tuttuğu ve bayt sayısının 16’nın sıfırdan farklı bir katı olduğu anlamına gelir; yani aktarım veriyi parçalamamış ve GCM’de değilsiniz. wrong final block length istisnadır. Orada sayı 16’nın katı değildi; bu da anahtarı değil kırpılmayı işaret eder, o yüzden 8. bölüme atlayın.
GCM Authentication failed ve benzerleri
GCM, NIST SP 800-38D’nin gerektirdiği gibi, düz metnin tek bir baytını bile bırakmadan önce etiketi karşılaştırır. Bu da onu, dolgu hatasının olmadığı bir anlamda dürüst kılar: (anahtar, nonce, şifreli metin, ek kimlik doğrulanmış veri, etiket) demetindeki bir şey, şifreleyen tarafın kullandığıyla uyuşmuyor. Hangi öğe olduğunu size söyleyemez ve hiçbir zaman da söylemeyecek, çünkü bunu daraltmak bilinçli olarak algoritmanın işinin dışında bırakılmıştır. 6. bölüm, diller arasında en sık bozulan öğeyi ele alıyor: etiketin değeri değil, konumu.
Hata yok ama çıktı bozuk
Tehlikeli sonuç bu, çünkü bir gösterge paneli bunu başarı olarak kaydeder. CTR asla istisna fırlatmaz, ECB de fırlatmaz. CBC yalnızca son bayt kalıbı dolgu denetimini geçemediğinde fırlatır; yanlış anahtarla o bayt fiilen rastgeledir, yani kabaca 256 denemeden biri 0x01 üzerine düşer ve geçerli sayılır. Yanlış anahtarlı CBC şifre çözmelerinin %0,4’ten biraz azı “başarılı olur”. Yine de bozuk çıktının bir biçimi vardır ve bu biçim hatanın adını koyar: ezberlemeye değer iki parmak izi 4. ve 5. bölümlerde.
2. AES’teki en yanıltıcı hata
Çoğu kişinin hata ayıklama önceliklerini yeniden sıralayan ölçüm şu. Anahtar 0123456789abcdef, tümü sıfır IV, AES/CBC/PKCS5Padding, düz metin hello world; java 1.8.0_162 üzerinde, JDK’nın yerleşik SunJCE sağlayıcısıyla:
| Senaryo | Değişiklik | Ölçülen sonuç |
|---|---|---|
| A | Anahtar 1 bayt yanlış (son karakter f → X) | javax.crypto.BadPaddingException: Given final block not properly padded fırlatır. Dolgunun kendisi hiçbir zaman bozuk değildi; hata tümüyle yanıltıcı |
| B | Anahtar doğru, IV 1 bayt yanlış | istisna yok, düz metin hello world geriye iello world olarak döndü. Yalnızca ilk bloğun karşılık gelen baytı bozuldu |
| C | Anahtar doğru, CBC şifreli metni AES/ECB ile çözme | sessizce başarılı oldu, istisna yok. Bir mod uyuşmazlığının herhangi bir şey yükseltmesi gerekmez |
A senaryosu koca öğleden sonraları yanlış yöne saptırır. C senaryosu üretime bozuk veri gönderir.
Yanlış anahtar neden dolgu hatası üretir
Dolguyla ilgili hiçbir şey yanlış değildi. Şifreleyen taraf, hello world’ü on altıya tamamlamak için beş adet 0x05 baytı ekledi, o bloğu şifreledi ve blok şifreli metninizde zarar görmemiş halde duruyor.
Arıza çıkış yolunda oluşur. CBC şifre çözme, blok şifresini tersine çalıştırır, her sonucu bir önceki şifreli metin bloğuyla XOR’lar ve ancak ondan sonra kaç baytın atılacağına karar vermek için son bloğun kuyruğunu okur. Yanlış anahtarla şifre on altı baytlık gürültü üretir ve gürültü neredeyse hiçbir zaman geçerli bir PKCS#7 kalıbıyla bitmez. Kütüphane gördüğü şeyi bildirir: bozuk dolgu. Bildirim doğrudur ve hiçbir işe yaramaz.
BadPaddingException’ı şöyle okuyun: “yeniden kurduğum düz metin, dolgulu düz metnin bittiği gibi bitmiyor”. Yeniden kurmanızın yanlış olmasının en olası nedeni anahtardır; aes decrypt wrong key aramasıyla bad padding exception aramasının sizi aynı forum başlıklarına düşürmesinin sebebi de budur: iki belirti aslında tek belirtidir. Buraya bir tasarım notu iliştirmek gerekiyor. Bu ayrımı çağıran tarafa asla göstermeyin, çünkü “dolgu geçersiz” ile “dolgu geçerli, içerik yanlış” arasındaki farkı ayırt edebilmek, padding oracle saldırısının beslendiği şeydir (Vaudenay, EUROCRYPT 2002).
GCM neyi farklı yapar
GCM sırayı tersine çevirir; herhangi bir düz metin üretmeden önce etiketi doğrular, yani kısmen doğru baytların var olduğu bir aralık hiç oluşmaz. GCM arızası sizi çıktının gerçek olup olmadığını merak eder halde bırakmaz, çünkü ortada çıktı yoktur. GCM’de ayrıca hiç dolgu yoktur, çünkü altta bir sayaç modu çalışır; şifreli metin uzunluğu da böylece düz metin uzunluğuna eşit olur. Bu yüzden GCM sandığınız bir sistemdeki dolgu hatası, o sistemin GCM olmadığını kanıtlar; genelde CBC’ye geri düşmüş bir yapılandırmadır.
3. İki taraf da aynı anahtar baytlarını mı kullanıyor?
AES sizin anahtar dizenizi görmez. 16, 24 veya 32 bayt görür. İki sistem bir yapılandırma dosyasında birebir aynı anahtar malzemesini tutup yine de anlaşamayabilir, çünkü “birebir aynı” olmak baytlara değil metne ait bir özelliktir.
Bir anahtar dizesinin bayta dönüşmesinin üç yolu
0123456789abcdef düz dizesini üç farklı kütüphaneye verin:
as hex -> 8 bytes (invalid AES key length)
as base64 -> 12 bytes (invalid AES key length)
as raw UTF-8 -> 16 bytes (valid AES-128)
On altı karakter, üç farklı bayt sayısı. Bu tam da her üç okumada da geçerli olduğu için can sıkıcı bir durum: her karakter hem hex hem Base64 alfabesinde bulunuyor ve on altı karakter iki çözücü için de yasal bir uzunluk, dolayısıyla ayrıştırma anında hiçbir şey hata vermiyor.
JWT invalid signature sorun giderme rehberinde her ekosistemin bir gizli anahtar dizesini nasıl yorumladığına dair tam kütüphaneler arası matris var; AES için kısa hali, anahtar malzemenizin hangi kodlamada olduğunu yazıya dökmek ve iki tarafın da açıkça çözmesini sağlamaktır. Aynı hatanın HMAC biçimi webhook alıcılarını vurur; webhook imza doğrulama rehberi bunu ele alıyor.
AES katıdır: tam olarak 16, 24 veya 32 bayt
AES, çoğu geliştiricinin ilk tanıştığı ilkelden burada ayrılır. HMAC her anahtar uzunluğunu kabul eder: RFC 2104, blok boyutundan uzun olan her şeyin özetini alır ve kısa olan her şeyi sıfırla doldurur; bu yüzden bir HMAC üretici 7 baytlık da 700 baytlık da bir gizli anahtarı sesini çıkarmadan alır. AES’in tam olarak üç yasal anahtar uzunluğu vardır ve tek bir blok bile işlenmeden diğer her şeyi reddeder.
Bu katılık işinize yarar, çünkü uzunluk hatası, dolgunun arkasına saklanmak yerine kendi nedenini adıyla söyleyen tek AES arızasıdır. Bizim aracımız bunu Key must be 16, 24, or 32 bytes (AES-128/192/256). şeklinde ifade eder. Yanlış uzunluk üreten tuzaklar:
- Sondaki satır sonu:
KEY=$(cat key.txt)ya daecho "$KEY"kaynaklı.printfveecho -nkullanın. Bir gizli anahtar yöneticisi arayüzünden yapıştırılan sondaki boşluk da aynı sonucu doğurur. 0xöneki: hata ayıklayıcıdan kopyalanmış otuz dört karakter artık geçerli hex değil.- ASCII dışı karakterler:
contraseña10 karakterdir ama UTF-8’de 11 bayttır, yani tek bir aksanlı harf içeren “32 karakterlik” bir parola 33 bayta çıkar.
SecretKeySpec ve platformun varsayılan karakter kümesi
Java’nın bunun ancak dağıtımdan sonra ortaya çıkan bir sürümü var. Argümansız "my secret".getBytes() çağrısı platformun varsayılan karakter kümesini kullanır; bu küme JDK 18’den önce file.encoding özelliğinden, dolayısıyla makinenin işletim sisteminden ve yerel ayarından gelirdi. UTF-8 kullanan bir dizüstü bilgisayar ile ANSI_X3.4-1968 kullanan bir konteyner, ASCII dışı her karakter için farklı baytlar üretir. JEP 400, JDK 18’de UTF-8’i varsayılan yaptı; ama düzelme yalnızca o sürümden sonra yazılan kodda geçerlidir.
// yanlış: baytlar makineye bağlıdır
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(), "AES");
// doğru: baytlar hiçbir şeye bağlı değildir
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "AES");
Kodunuz yerelde çalışıp sunucuda dolgu hatasıyla başarısız oluyorsa ve parola ASCII dışında bir şey içeriyorsa, önce bunu kontrol edin.
4. IV: nereye konur ve yanlışı nasıl görünür
aes iv mismatch, insanların ilk şüphelendiği ama en son teşhis ettiği arızadır, çünkü diğerleri gibi davranmaz. Sessizdir ve yereldir.
Yanlış IV tam olarak bir bloğu bozar
B senaryosuna tekrar bakın. Anahtar doğru, IV bir bayt yanlış:
hello world -> iello world
İstisna yok, tek karakter. İlk blok için CBC adımını yazın, apaçık ortaya çıkar: P1 = D(C1) XOR IV. IV doğrudan ilk düz metin bloğuyla XOR’lanır ve başka hiçbir şeye dokunmaz; yani IV’nin bir bitini çevirmek, düz metnin aynı konumdaki aynı bitini çevirir. Burada h (0x68), i (0x69) oldu; demek ki IV’nin ilk baytı tam olarak 0x01 kadar kaymış.
Buradan bir parmak izi çıkıyor. CBC’de ilk 16 baytın bozuk, sonrasındaki her şeyin temiz olması IV’nin yanlış, anahtarın doğru olduğu anlamına gelir. Her blok bozuksa anahtar yanlıştır. Bu tek gözlem, tek satır kod değiştirmeden en yaygın iki nedeni birbirinden ayırır; AES şifre çözme aracı da çözülmüş baytları gösterdiği için bunu doğrudan okuyabilirsiniz.
Peki neden hiçbir şey fırlatmadı? hello world 11 bayttır, yani tek bir bloktur ve PKCS#7 dolgusu bu bloğun 11 ile 15 arasındaki baytlarında durur. Değişen IV baytı 0. bayttı, dolayısıyla dolgu bölgesine dokunulmadı ve bölge doğrulandı. 11. ya da sonraki bir konumdaki IV baytını bozarsanız bunun yerine bir dolgu hatası alırsınız; dolgu hatasının size yalan söylediği bir başka yol da budur.
Üç aktarım geleneği
IV’nin nereye konacağına dair bir standart yok, yalnızca birbiriyle kötü çalışan üç alışkanlık var.
En yaygın gelenek başa eklemek: iv || ciphertext, bizim araçlarımızdaki varsayılan da bu. İki taraf da ne kadarının kırpılacağı konusunda anlaşmalı: CBC ve CTR için 16 bayt, GCM için 12. Aynanın öbür yüzündeki hata, başa ekleyen bir gönderici ile eklemeyen bir alıcıdır. O zaman “şifreli metnin” ilk 16 baytı aslında IV olur, her blok kayar ve bir dolgu hatası alırsınız.
İkincisi IV’yi ayrı bir alanda taşımak. {"iv": "...", "ciphertext": "..."} ilkesel olarak daha temizdir ve bir kodlamanın uyuşmayabileceği yerlerin sayısını ikiye katlar, çünkü artık IV’nin de kendi Base64 mi hex mi sorusu vardır.
Üçüncüsü sabit bir değer, genelde tümü sıfır, birinin belirlilik ihtiyacı yüzünden koda gömülmüş. Kusursuz biçimde uyumlu çalışır ve tehlikeli olmasının nedeni de tam budur: CBC’de sabit bir IV kayıtlar arasındaki eşitliği sızdırır, GCM’de ise tek bir anahtar altında bir nonce’ı yeniden kullanmak iki düz metnin XOR’unu açığa çıkarır ve etiketi doğrulayan GHASH alt anahtarını ifşa edebilir. SP 800-38D benzersizlik konusunda açıktır.
Aracın çıplak şifreli metin anahtarı ile açık IV geçersiz kılma seçeneği, aynı baytlar üzerinde üç geleneği de bir dakikada sınar.
GCM’nin IV’si 16 değil 12 bayttır
GCM’ye mevcut bir CBC yolunu düzenleyerek geçen ekipler 16 baytlık IV’yi olduğu gibi taşır ve sonuç hiçbir ipucu vermeden başarısız olur.
SP 800-38D 96 bitlik bir IV’yi standartlaştırır. Başka uzunluklara izin verilir ama bunlar yalnızca “daha uzun bir IV” değildir: IV 96 bit olmadığında GCM, başlangıç sayaç bloğunu IV’yi doğrudan kullanmak yerine GHASH’ten geçirerek türetir. Dolayısıyla nonce olarak kullanılan aynı 16 bayt, ilk 12 baytın üreteceğinden tamamen farklı bir anahtar akışı ve etiket üretir; siz de genel bir kimlik doğrulama hatası alırsınız. Şifreli metin başka bir yerden geldiyse ve yerleşimi tahmin etmeye çalışıyorsanız sondan sayın: etiket son 16 bayttır, nonce ise neredeyse her zaman ilk 12 bayttır.
5. Mod uyuşmazlığı, sessiz olanı dahil
Cipher.getInstance("AES") ECB’dir
Java, bir şifreyi mod ya da dolgu şeması belirtmeden adlandırmanıza izin verir. Reddetmez ve uyarmaz. JDK’nın yerleşik SunJCE sağlayıcısı altında boşlukları ECB ve PKCS5Padding ile doldurur.
Bunu kanıtlamak doğru deneyi gerektirir: 0123456789abcdef anahtarıyla 32 özdeş baytı (iki blok A) şifreleyin, sonra iki şifreli metin bloğunun eşleşip eşleşmediğine bakın. java 1.8.0_162 üzerinde:
getInstance("AES") ciphertext = 3bfd04cc0d7ed55358e2cbe19de213833bfd04cc0d7ed55358e2cbe19de21383377222e061a924c591cd9c27ea163ed4
block1 = 3bfd04cc0d7ed55358e2cbe19de21383
block2 = 3bfd04cc0d7ed55358e2cbe19de21383 <- identical blocks = the ECB fingerprint (plaintext structure leaks)
getInstance("AES/CBC/PKCS5Padding") blocks differ = chaining is active
Bayt bayt özdeş. Bu, ECB imzasıdır; ünlü şifrelenmiş penguen görselinin hâlâ penguen gibi görünmesini sağlayan özelliğin ta kendisi. Deney yalnızca özdeş düz metin bloklarıyla işe yarar: on altı A baytının ardından gelen on altı B baytı ECB altında da iki farklı şifreli metin bloğu üretir ve varsayılanın CBC olduğu sonucuna yanlışlıkla varırsınız.
Sonucun kapsamını dikkatli çizin: bu, yukarıdaki sürümdeki JDK’nın yerleşik SunJCE sağlayıcısını anlatır. Varsayılan dönüşüm bir sağlayıcı kararıdır, dolayısıyla BouncyCastle gibi üçüncü taraf bir sağlayıcı aynı kısaltmayı farklı çözebilir. Çıkarılacak genelleme “Java demek ECB demektir” değil şudur: niteliksiz bir dönüşüm dizesi, sağlayıcınız ne kararlaştırdıysa odur. Böyle bir dizeyi hiç yazmamanızın sebebi de budur.
Yanlış mod hata yükseltmeyebilir
C senaryosu, CBC şifreli metni AES/ECB ile çözdü ve hiçbir istisna vermeden doğru düz metni döndürdü. Aritmetiği yazana kadar bu imkânsız görünür. İlk bloğun CBC ile şifrelenmesi C1 = E(P1 XOR IV), o bloğun ECB ile çözülmesi ise D(C1) = P1 XOR IV demektir. Buradaki IV tümüyle sıfır olduğundan P1 XOR 0 = P1 olur ve ilk blok kusursuz çözülür. hello world bir blok uzunluğunda olduğu için “ilk blok” mesajın tamamıydı.
Akılda tutulacak kural şu: sıfır IV ile ECB ve CBC ilk blokta anlaşır, ondan sonraki her blokta ayrışır. Uzun bir CBC mesajını ECB olarak çözerseniz on altı temiz baytın ardından gürültü gelir; bu, yanlış IV parmak izinin tam tersidir. Zıt iki biçim, iki farklı hata ve hiçbirinde hata mesajı yok. Koda gömülü sıfır IV’ler, bunu bir laboratuvar merakı olmaktan çıkaracak kadar yaygındır.
Her dilde en sade çağrı size ne verir
| Ekosistem | En sade çağrı | Gerçekte aldığınız mod |
|---|---|---|
| Java (SunJCE) | Cipher.getInstance("AES") | sessizce, PKCS5Padding’li ECB |
Node crypto | createDecipheriv('aes-256-cbc', key, iv) | algoritma dizesi ne diyorsa o; varsayılan diye bir şey yok |
| Web Crypto | crypto.subtle.decrypt({ name: 'AES-CBC', iv }, ...) | açıkça adlandırılır; ECB hiç gerçeklenmemiştir |
Python cryptography | Cipher(algorithms.AES(key), modes.CBC(iv)) | mod nesnesi zorunludur |
| PyCryptodome | AES.new(key, AES.MODE_ECB) | zorunlu argüman, ama ECB otomatik tamamlamanın tam orta yerinde duruyor |
Go crypto/aes | aes.NewCipher(key) ham bir cipher.Block döndürür | o blok üzerinde Decrypt çağırmak ECB’nin ta kendisidir; onu cipher.NewCBCDecrypter ya da cipher.NewGCM ile sarın |
| CryptoJS | CryptoJS.AES.decrypt(ct, "passphrase") | CBC, PKCS#7, MD5 ile EVP_BytesToKey (bkz. 7. bölüm) |
Modun bir dizede ya da bir nesnede yaşadığı ekosistemler sizi asla şaşırtmaz. “Sadece AES” çağrısı sunan ikisi, yani Java ve Go, kazara ECB bildirimlerinin geldiği yerdir. Şüpheniz varsa aynı baytları her modda tek tek çözerek hangisinin tuttuğunu doğrudan görebilirsiniz.
6. GCM: aynı baytlar, farklı API’ler
Diller arası aes gcm auth tag arızalarının çoğu kriptografik değildir. İki taraf da aynı 16 baytı hesaplamıştır ve yalnızca o baytların nerede durduğu konusunda anlaşamazlar.
Ölçüm
Anahtar = 32 bayt 0123456789abcdef0123456789abcdef, IV = 12 sıfır bayt, düz metin hello world; node v25.8.2 ve java 1.8.0_162 üzerinde:
Node ciphertext = a616cd6d7d2328379d41e5 (11 B) <- update+final
authTag = c87af9f8ad7148e873fa797292c0af3f (16 B) <- fetched separately via getAuthTag()
Java doFinal() = a616cd6d7d2328379d41e5c87af9f8ad7148e873fa797292c0af3f (27 B) <- ciphertext and tag already concatenated
Node ciphertext || authTag, tam olarak Java doFinal() sonucudur, 27 baytın tamamı. Kodlama farkı yok, pazarlık edilecek bir şey yok: Node iki parçayı size ayrı ayrı verir, Java ise yapıştırılmış halde. Ayrıca 11 baytlık düz metnin 11 baytlık şifreli metin verdiğine dikkat edin; çünkü GCM hiç dolgu eklemez. Bu yüzden gerçek bir GCM yolundan asla dolgu hatası gelmez.
Çalışma ortamına göre birleşik mi ayrı mı
| Runtime | Şifreleme API’si | Etiketin nereye düştüğü |
|---|---|---|
Node crypto | update() + final(), sonra getAuthTag() | ayrı |
Java (SunJCE, AES/GCM/NoPadding) | doFinal() | sona eklenmiş |
Go cipher.AEAD | Seal() | sona eklenmiş |
Python cryptography, AESGCM | encrypt() | sona eklenmiş |
Python cryptography, Cipher + modes.GCM | finalize(), sonra encryptor.tag | ayrı |
| Web Crypto | crypto.subtle.encrypt | sona eklenmiş |
Üst düzey API’ler arasında sıra dışı olan Node’dur ve Node’dan herhangi bir yere giden yön, en çok bildirilen arıza yönüdür. Node çıktısını bir Java, Go, Python ya da tarayıcı tüketicisi için paketlemek:
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()]); // artık doFinal() ile eşleşiyor
Birleştirilmiş bir yığını Node için açmak:
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(packed.subarray(packed.length - 16)); // final() çağrısından önce gelmeli
const pt = Buffer.concat([
decipher.update(packed.subarray(0, packed.length - 16)),
decipher.final(),
]);
Sıralama kısıtı gerçektir: final() çağrısından sonra setAuthTag() çağırırsanız, her bayt doğru olsa bile Node Unsupported state or unable to authenticate data fırlatır. Etiketin yığında nerede durduğundan emin değilseniz, iki yerleşimi de aynı baytlar üzerinde deneyip hangisinin doğrulandığına bakın.
Etiket uzunluğu değişkendir, birimi değil
GCM 128, 120, 112, 104 veya 96 bitlik etiketlere izin verir; 64 ve 32 ise kısıtlı uygulamalar için ayrılmıştır (SP 800-38D, Ek C). Neredeyse herkes 128 kullanır ve asıl sorun her API’nin bunu nasıl istediğidir:
Java’da new GCMParameterSpec(128, iv) yazarsınız ve ilk argüman bit cinsindendir. Web Crypto’da { name: 'AES-GCM', iv, tagLength: 128 } geçirirsiniz; o da bit, varsayılanı 128. Node ise createCipheriv(algo, key, iv, { authTagLength: 16 }) ile bayt ister.
new GCMParameterSpec(16, iv), 16 bitlik bir etiket isteyen ve yasal görünen bir Java satırıdır; bazı JDK’lar bunu reddeder, kabul edildiği yerlerde ise bütünlük garantinizi 65.536’da birlik bir yazı tura atışıyla takas etmiş olursunuz. İki taraf etiket uzunluğunda anlaşamadığında paketlenmiş uzunluklar da farklı olur; alıcı yanlış sınırdan keser ve anahtarla hiçbir ilgisi olmayan bir kimlik doğrulama hatası alır.
7. Elinizde anahtar değil, parola var
Taraflardan biri insan eliyle yazılmış bir dize alıyorsa, o dize ile AES arasında bir anahtar türetme fonksiyonu vardır ve KDF uyuşmazlığı görünmezdir. Asla hata vermez. Kusursuz 32 bayt döndürür, sadece bunlar yanlış 32 bayttır; arıza da bir katman aşağıda (bunu artık biliyorsunuz) bir dolgu hatası olarak yüzeye çıkar.
PBKDF2’nin hizalanması gereken dört şeyi var
- Salt: OpenSSL’in
Salted__biçiminde şifreli metnin içinde 8 bayttır; bizim araçlarımızın parola biçiminde 16 baytlık bir önektir; elle yazılmış şemalarda ise sıklıkla koda gömülü bir sabittir. - Yineleme sayısı:
openssl enc -pbkdf2varsayılan olarak 10.000 kullanır. OWASP şu anda PBKDF2-HMAC-SHA256 için 600.000 öneriyor; bizim parola modumuzun kullandığı da bu. Framework’ler kendi sayılarını seçer. - Özet fonksiyonu: SHA-1’e karşı SHA-256’ya karşı SHA-512. Eski kodlar ve bazı mobil SDK’lar hâlâ varsayılan olarak SHA-1 kullanıyor.
- Çıktı uzunluğu: AES-256 için otuz iki bayt, AES-128 için on altı. Bazı şemalar anahtarı ve IV’yi tek bir daha uzun çağrıdan birlikte türetir; bu da düz 32 baytlık bir türetmeyle asla eşleşmez.
EVP_BytesToKey ve CryptoJS neden çalışmamaya devam ediyor
cryptojs aes decrypt not working genelde tek bir belirli uyuşmazlıktır. CryptoJS.AES.encrypt(text, "passphrase") PBKDF2 kullanmaz. OpenSSL’in 1.1 öncesi türetmesi olan EVP_BytesToKey’i, MD5 ve tek bir yinelemeyle kullanır.
EVP_BytesToKey ayrıca PBKDF2’nin yapmadığı bir şey daha yapar: anahtarı ve IV’yi birlikte, paroladan ve salt’tan tek geçişte türetir. OpenSSL’in Salted__ dosyası bu yüzden ayrı bir IV alanı taşımaz, CryptoJS çıktısını PBKDF2 artı rastgele bir IV ile yeniden üretmek de bu yüzden iki kez birden yanlıştır.
Biçim ilk bakışta tanınır: 8 ASCII baytlık Salted__ ve ardından gelen 8 baytlık salt, Base64 ile kodlandığında her zaman U2FsdGVkX1 ile başlar. Şifreli metniniz böyle başlıyorsa paroladan türetilmiştir ve hangi türetmenin kullanıldığını bilmeniz gerekir; AES şifre çözme aracı bu öneki algılar ve üçü arasında kod değişikliği olmadan geçiş yapar.
Aynı parola neden farklı anahtarlar verir
“AES parolası” diye bir şey yoktur. Her kütüphane dizeden anahtara giden kendi yolunu icat etti:
| Üreten taraf | Türetme | Tek bir parola için sonuç |
|---|---|---|
CryptoJS AES.encrypt(text, pass) | EVP_BytesToKey, MD5, 1 yineleme | anahtar A |
openssl enc 1.0.2 ve öncesi | EVP_BytesToKey, MD5, 1 yineleme | anahtar A |
openssl enc 1.1+, -pbkdf2 olmadan | EVP_BytesToKey, SHA-256, 1 yineleme | anahtar B |
openssl enc -pbkdf2 | PBKDF2-HMAC-SHA256, 10.000 yineleme | anahtar C |
| Bizim parola modumuz | PBKDF2-HMAC-SHA256, 600.000 yineleme | anahtar D |
| Java, Python, Go | hiç varsayılan yok; türetmeyi siz yazarsınız | ne yazdıysanız o |
Henüz kimse hata yapmadan, tek bir paroladan dört anahtar. 1.0.2’den 1.1’e yükseltme varsayılan özet fonksiyonunu MD5’ten SHA-256’ya çevirdi; eski betiklerden çıkan şifreli metnin daha yeni bir makinede aynı komutla çözülmemeye başlamasının sebebi bu. Devraldığınız bir veri varsa ve araç zincirini kimse hatırlamıyorsa türetmeleri bu sırayla deneyin. Üç seçenekli bir sınama bu; hepsini denemek birkaç dakika sürer.
8. Aktarım baytlarınıza ne yaptı
Şifreli metin, düzgün dağılmış rastgele ikili veridir; bu da onu baytları metin gibi ele alan her şeye karşı azami ölçüde düşman kılar. AES arızalarının büyük bir bölümü şifreye hiç uğramaz.
Base64 varyantları ve eksik dolgu
Standart Base64 (RFC 4648 §4) + ve / kullanır; URL-güvenli varyant (§5) - ve _ kullanır. Standart bir çözücüye verilen URL-güvenli bir dize ya hata fırlatır ya da hoşgörülü çözücülerde sorunlu karakterleri sessizce atıp kısa ve hizası kaymış baytlar döndürür; Java’nın Base64.getUrlDecoder() ile Base64.getDecoder() işlevlerini ayrı nesneler halinde sunması bundandır. Bazı kodlayıcılar ayrıca sondaki = işaretini atar, bazı çözücüler ısrarla ister ve JWT’ye komşu kod yolları varsayılan olarak onu kırpar.
Anahtardan şüphelenmeden önce şifreli metni çözün ve uzunluğunu moda göre denetleyin:
- CBC ve ECB: 16’nın sıfırdan farklı bir katı. Başka bir sayı görüyorsanız anahtarı bırakın, kırpılmaya ve çözme adımına bakın.
- GCM: şifreli metin uzunluğu düz metin uzunluğuna eşittir, artı etiket için 16, nonce başa eklenmişse başta artı 12.
- CTR: herhangi bir uzunluk, dolayısıyla bu denetim size hiçbir şey söylemez.
Base64 çözücü bayt sayısını tek bir yapıştırmayla verir; bu genelde tüm soruşturmanın en hızlı ölçümüdür.
Satır sonları, akıllı tırnaklar ve UTF-8 gidiş-dönüşü
openssl base64, -A vermezseniz çıktıyı 64 sütunda satır başına geçirir; bazı çözücüler gömülü satır sonlarını atlarken bazıları reddeder, yani aynı dosya bir makinede çözülür, başka bir makinede başarısız olur. Bir sohbet istemcisi ya da belge düzenleyicisi üzerinden kopyalamak düz tırnakları kıvrık, kısa çizgileri de uzun çizgi yapar ve fark bir terminalde neredeyse görünmez.
Geri döndürülemez olanı UTF-8 gidiş-dönüşüdür. Ham AES çıktısı önce kodlanmadan herhangi bir yerde karakter dizisi olarak tutulursa (Java’da new String(cipherBytes), Python’da bytes.decode('utf-8', errors='replace'), herhangi bir yerde bir TextDecoder), geçerli UTF-8 olmayan her bayt dizisi U+FFFD’ye çöker ve geri kodladığınızda verinizin bulunduğu yerde EF BF BD bulursunuz. Rastgele baytların kabaca yarısı ASCII dışı olduğundan şifreli metnin çoğu yok olur ve hiçbir anahtar onu geri getirmez; UTF-8 ve UTF-16 kodlama rehberi kaybın neden tek yönlü olduğunu anlatıyor. İkili şifreli metni Base64, hex ya da ham ikili olarak taşıyın; karakter dizisine hiç dönüştürmeyin.
Veritabanı sütunları
Depolama aynı hasarı daha sessizce verir. Bir blok fazla uzun olduğu için VARCHAR(255) alanına yazılan şifreli metin kesilir ve MySQL katı mod dışında bunu hatasız yapar. Dolgu bloğu ile GCM etiketinin yaşadığı yer tam da kuyruktur; yani aylar önce “başarıyla” yazılmış bir satır şimdi başarısız olur ve kesim 16 baytlık bir sınıra denk geldiyse yukarıdaki uzunluk denetimi de bunu yakalayamaz. Gerisini karakter kümesi dönüşümü halleder: UTF-8 baytları alan bir latin1 sütunu verinizi girişte yeniden yazar.
Şifreli metni VARBINARY, BLOB ya da bytea içinde saklayın; ya da Base64’ü bol yer bırakılmış bir metin sütununda saklayın.
9. Beş dakikada bulan bir ikiye bölme akışı
Yukarıdaki her bölüm bir değişkeni daraltır. Bunları kontrolünüzdeki bir referans gerçeklemeye karşı sırayla çalıştırmak hızla yakınsar; tarayıcı araçları da bu referans olarak iyi iş görür, çünkü tamamen tarayıcınızda çalışır, anahtarınız ve şifreli metniniz sayfadan çıkıp bir sunucuya gitmez ve her seferinde tek bir ayarı değiştirip baytları görebilirsiniz.
-
Adım 0, biçimi ölçün. Şifreli metni çözün ve bayt sayısını, ilk birkaç baytı ve
U2FsdGVkX1ile başlayıp başlamadığını not edin. Sayıyı 8. bölüme göre denetleyin. 16’nın katı değilse ve CBC’de olduğunuza inanıyorsanız durun: bu bir aktarım hatasıdır. -
Adım 1, bilinen bir düz metni şifreleyin. AES şifreleme aracında, üretimin kullandığına inandığınız parametrelerle kısa ve bilinen bir dizeyi şifreleyin, sonra iki çıktının değerlerini değil biçimlerini karşılaştırın: toplam uzunluk, önek baytları, salt başlığının varlığı. Uyuşmazlık, biçim ya da KDF hakkındaki varsayımınızın yanlış olduğu anlamına gelir ve anahtarla ne kadar oynarsanız oynayın bunu düzeltemezsiniz.
-
Adım 2, türetmeleri sırayla deneyin. Paroladan türetilmiş veri için AES şifre çözme aracında önce tam yineleme sayısıyla PBKDF2’yi, sonra EVP-SHA256’yı, sonra EVP-MD5’i çalıştırın. Yalnızca biri doğru olabilir. Hiçbiri işe yaramazsa hata KDF’nin üstündedir.
-
Adım 3, bütün gelenekleri kaldırın. Ham anahtara geçin, çıplak şifreli metni açın, IV’yi açıkça verin. Artık hangi baytların anahtar, IV ve şifreli metin olduğunu hiçbir şey çıkarsanmadan tam olarak siz bildiriyorsunuz. Burada çözülüyor ama kodunuzda çözülmüyorsa hatanız kriptografik değil, çerçeveleme hatasıdır (kırpılmamış bir IV öneki, yanlış yerde bir etiket).
-
Adım 4, modu değiştirin. Aynı baytlarda önce CBC’yi, sonra CTR’yi, sonra GCM’yi deneyin. CBC’nin başarısız olduğu yerde CTR’nin okunabilir metin döndürmesi mod uyuşmazlığından başka bir şey olamaz.
-
Adım 5, bozuk çıktıyı okuyun. İlk blok bozuk, geri kalanı temizse sorun IV’dir. İlk blok temiz, geri kalanı bozuksa CBC’yi sıfır IV ile ECB olarak çözmüşsünüzdür. Her şey bozuksa sorun anahtar ya da türetmedir.
10. SSS
AES kodum neden yerelde çalışıyor da üretimde başarısız oluyor?
Kod yerelde çalışıp üretimde başarısız oluyorsa ortam, sürüm kontrolünde olmayan bir şeyi değiştirmiştir. Olağan şüpheliler, sırayla: anahtar bir ortam değişkeninden ya da gizli anahtar yöneticisinden sonunda satır sonuyla geldi; Java’nın platform varsayılan karakter kümesi dizüstü bilgisayarla konteyner arasında farklı, dolayısıyla getBytes() farklı baytlar üretti (3. bölüm); üretimdeki OpenSSL 1.1+ iken yerel betikleriniz 1.0.2’yi hedefliyordu ve bu, EVP_BytesToKey özet fonksiyonunu MD5’ten SHA-256’ya çevirdi; ya da bir veritabanı sütunu şifreli metni yalnızca tek bir ortamda kırpıyor. Önce iki tarafta da anahtar uzunluğunu ve şifreli metin uzunluğunu bayt cinsinden yazdırın, çünkü bu iki sayı meseleyi genelde tek başına çözer.
Node’da şifreledim, Java’da çözemiyorum. Nereden başlamalıyım?
Node’dan Java’ya geçen bir şifreli metinde en yaygın ve en az göze çarpan nedenle, GCM etiketiyle başlayın. Node şifreli metni ve etiketi ayrı döndürür; Java’nın doFinal() çağrısı onları ciphertext || tag olarak birleştirilmiş bekler ve 6. bölüm baytların bunun dışında özdeş olduğunu gösteriyor. Bunun yerine CBC’deyseniz IV geleneğiyle başlayın: Node onu başa ekledi mi ve Java tarafı çözmeden önce 16 baytı kırpıyor mu? Üçüncüsü anahtarın kendisi; Buffer.from(k, 'hex') ile k.getBytes(StandardCharsets.UTF_8) aynı dizeden farklı uzunluklar üretir.
Java’nın PKCS5Padding’i PKCS#7 ile aynı mı?
Java’nın PKCS5Padding’i AES için fiilen PKCS#7 ile aynıdır. PKCS#5 (RFC 8018) yalnızca 8 baytlık bloklar için tanımlıdır; PKCS#7 (RFC 5652) şemayı 1 ile 255 bayt arasındaki blok boyutlarına genelleştirir. Java’nın 16 baytlık bir blok şifresine uygulanan PKCS5Padding’i PKCS#7 davranışını gerçekler ve isim tarihsel bir kalıntıdır, yani bu asla sizin hatanız olmaz. NoPadding olabilir: düz metnin zaten 16’nın katı olmasını gerektirir ve şifre çözerken dolguyu veri olarak geri verir, dolayısıyla sonunda \x05\x05\x05\x05\x05 gibi baytlar bulunan, makul görünen bir metin görürsünüz.
Anahtarım 32 karakter ama AES anahtar uzunluğunun geçersiz olduğunu söylüyor. Neden?
Uzunluk hatası, kütüphanenin 16, 24 ya da 32 olmayan bir bayt sayısı aldığı anlamına gelir. 32 karakterlik bir dizede bu genelde sondaki bir satır sonu (33 bayt), dizeyi geçersiz hex yapan bir 0x öneki ya da UTF-8’de iki veya üç bayt kaplayan ASCII dışı bir karakterdir. Daha tehlikeli varyantı hiç hata almamaktır: 32 hex karakteri 16 geçerli bayta, 32 Base64 karakteri 24 geçerli bayta çözülür ve ikisi de yasal AES uzunluklarıdır. Kütüphane bunları kabul eder, yanlış anahtarı kullanır ve size bunun yerine bir dolgu arızası verir. Karakter sayısını değil, bayt sayısını denetleyin.
Şifre çözme “başarılı oldu” ama çıktı bozuk. Ne yanlış gitti?
Şifre çözme başarılı görünürken çıktı bozuksa hiçbir şeyi doğrulamayan bir moddasınız. CTR ve ECB asla istisna fırlatmaz; CBC ise yalnızca son bayt kalıbı dolgu denetimini geçemediğinde fırlatır ve yanlış bir anahtar bu denetimi %0,4’ten biraz düşük bir olasılıkla geçer. Biçimi okuyun: ilk 16 bayt bozuk, geri kalanı temizse sorun IV’dir; ilk 16’sı temiz, geri kalanı bozuksa CBC şifreli metni sıfır IV ile ECB olarak çözmüşsünüzdür; her yeri aynı ölçüde bozuksa sorun anahtar ya da türetmedir. Sonunda birkaç tuhaf bayt bulunan okunabilir metin, dolgulu veri üzerinde NoPadding demektir. Uzun vadeli çözüm GCM’dir; böylece “başarılı oldu” bir anlam taşır.
IV’yi kaybettiysem yine de şifreyi çözebilir miyim?
IV’yi kaybettiyseniz CBC’de ilk 16 bayt dışındaki her şeyin şifresini yine de çözebilirsiniz. 2. bloktan itibaren her blok D(C_i) XOR C_{i-1} olarak geri getirilir ve bunun bütün girdileri zaten şifreli metnin içindedir, yani IV’ye yalnızca ilk blok ihtiyaç duyar. Düz metnin nasıl başladığını da biliyorsanız (diyelim {"userId": ile başlayan bir JSON yığını) IV’yi doğrudan D(C1) XOR P1 olarak geri kazanabilirsiniz. CTR’de IV bütün anahtar akışını tohumlar, dolayısıyla onu kaybetmek her şeyi kaybetmektir. GCM’de ise nonce hem sayacı hem etiketi besler ve kısmi kurtarma diye bir şey kalmaz.
GCM etiketi kırpıldıysa ya da düştüyse düz metni kurtarabilir miyim?
GCM etiketi kırpılmış ya da düşmüşse düz metni matematiksel olarak kurtarabilirsiniz, pratikte biraz emekle. GCM alttan CTR modudur, dolayısıyla anahtar ile nonce tek başına anahtar akışını yeniden üretir. Ana akım hiçbir kütüphane bunu sizin için yapmaz: Java, Go, Python ve Web Crypto, tasarım gereği geçerli bir etiket olmadan düz metni bırakmayı reddeder. Geçici çözüm, aynı baytları, başlangıç sayaç bloğu 12 baytlık nonce’ın ardından 00000002 gelecek şekilde ayarlanmış AES-CTR olarak çözmektir; GCM’nin ilk veri bloğu tam da orada başlar. Veriyi geri alırsınız ve bütün bütünlük garantilerinden vazgeçersiniz, o yüzden sonucu güvenilmez sayın. 16 etiket baytının hepsi hâlâ elinizdeyse ve kimlik doğrulama yine de başarısız oluyorsa, etiket eksik değildir ve bu sayfadaki başka bir şey sizin hatanızdır. Onu AES şifre çözme aracına götürün ve 0. adımdan başlayın.