Skip to content
Zurück zum Blog
Sicherheit

AES-Entschlüsselung fehlgeschlagen: Schlüssel, IV, Modus

AES-Entschlüsselung fehlgeschlagen? Ein falscher Schlüssel wirft einen Padding-Fehler, ein falscher IV zerstört nur Block eins. Kostenlos online debuggen.

16 Min. Lesezeit

AES-Entschlüsselung fehlgeschlagen: Schlüssel, IV, Modus, Padding

Wenn in Ihren Logs eine AES-Entschlüsselung fehlgeschlagen ist, beschreibt die Meldung, die Sie bekommen haben, vermutlich das falsche Problem. Vier voneinander unabhängige Fehler erzeugen fast identische Symptome, und der häufigste davon, ein falscher Schlüssel, meldet sich als Padding-Fehler.

Diese Rangfolge spart am meisten Zeit, ausgehend von einer CBC-Entschlüsselung, die BadPaddingException wirft:

  1. Die Schlüsselbytes unterscheiden sich zwischen den beiden Seiten. Mit deutlichem Abstand die wahrscheinlichste Ursache.
  2. Die Schlüsselableitung unterscheidet sich: gleiche Passphrase, aber anderes KDF oder andere Iterationszahl, also andere Schlüsselbytes.
  3. Der Transport hat den Chiffretext beschädigt, ihn also abgeschnitten, im Base64 verstümmelt oder durch eine Textkodierung hin- und zurückgeschickt.
  4. Der IV ist falsch. Das kommt vor, wirft aber keinen Padding-Fehler; er beschädigt sechzehn Bytes und bleibt dabei stumm.

Die Reihenfolge ist strukturell bedingt. CBC prüft das Padding als letzten Schritt der Entschlüsselung, nachdem es den Schlüssel angewendet und die Verkettung aufgelöst hat. Damit ist das Padding eine Prüfsumme über alles, was davor passiert, und es schlägt lautstark fehl, ganz gleich welches Glied gebrochen ist. Die Messungen dazu stehen weiter unten. Haben Sie es eilig, fügen Sie Ihren Chiffretext in das AES-Entschlüsselungstool ein und arbeiten die Bisektion aus Abschnitt 9 durch.

Alle folgenden Messungen stammen von java 1.8.0_162, node v25.8.2 und openssl 3.6.2. Vorgabewerte verschieben sich zwischen Versionen, betrachten Sie die Versionsnummern also als Teil des Ergebnisses.

1. Beginnen Sie damit, was Ihr Fehler tatsächlich ausschließt

Eine AES-Fehlermeldung sagt fast nichts über die Ursache und sehr viel darüber, was die Ursache nicht sein kann. Nutzen Sie sie, um Zweige zu streichen, nicht um einen auszuwählen.

Was Sie sehenWas das ausschließtWas noch möglich ist
BadPaddingException, bad decrypt, wrong final block lengthGCM; ein reiner IV-Fehler; ein Dekodierfehlerfalscher Schlüssel, falsches KDF, abgeschnittener Chiffretext, als Chiffretext mitverarbeitete IV-Bytes, Modus-Verwechslung, abweichendes Padding-Verfahren
GCM Authentication failed, Unsupported state or unable to authenticate dataPadding; jede Theorie, die eine Teilausgabe voraussetztfalscher Schlüssel, falsche Nonce, abgetrennter oder falsch platzierter Tag, falsche Tag-Länge, abweichende AAD
Keine Exception, die Ausgabe ist Mülljeder authentifizierte ModusECB, CTR, CBC mit Glück, Modus-Verwechslung, falscher IV

BadPaddingException, bad decrypt, wrong final block length

Das ist dasselbe Ereignis in drei Ökosystemen: Java, OpenSSL und .NET. Es tritt am Ende einer CBC- oder ECB-Entschlüsselung auf, wenn der letzte Klartextblock nicht auf ein gültiges PKCS#7-Muster endet.

Nützlich ist daran die Verneinung: Wenn Sie so weit gekommen sind, hat sich Ihr Base64 oder Hex dekodieren lassen und die Byte-Anzahl war ein Vielfaches von 16 ungleich null. Der Transport hat die Daten also nicht zerschreddert, und Sie sind nicht in GCM. wrong final block length ist die Ausnahme: Dort war die Anzahl kein Vielfaches von 16, was eher auf eine Kürzung als auf den Schlüssel deutet. Springen Sie in dem Fall zu Abschnitt 8.

GCM Authentication failed und Verwandte

GCM vergleicht den Tag, bevor es auch nur ein einziges Byte Klartext herausgibt, so wie NIST SP 800-38D es verlangt. Das macht die Meldung auf eine Weise ehrlich, wie der Padding-Fehler es nie ist: Irgendetwas im Tupel (Schlüssel, Nonce, Chiffretext, zusätzliche authentifizierte Daten, Tag) passt nicht zu dem, was die verschlüsselnde Seite verwendet hat. Welches Element es ist, kann GCM Ihnen nicht sagen und wird es nie können, denn diese Eingrenzung liegt bewusst außerhalb dessen, was der Algorithmus leistet. Abschnitt 6 behandelt das Element, das über Sprachgrenzen hinweg am häufigsten bricht: die Position des Tags, nicht seinen Wert.

Kein Fehler, aber die Ausgabe ist Müll

Das ist der gefährliche Ausgang, weil ein Dashboard ihn als Erfolg verbucht. CTR wirft nie eine Exception, ECB ebenso wenig. CBC wirft nur dann, wenn das letzte Byte-Muster die Padding-Prüfung nicht besteht, und bei falschem Schlüssel ist dieses Byte praktisch zufällig. Ungefähr ein Versuch von 256 landet also auf 0x01, und die Bibliothek lässt ihn durchgehen. Knapp unter 0,4 % der CBC-Entschlüsselungen mit falschem Schlüssel „gelingen“. Müll hat allerdings eine Form, und diese Form benennt den Fehler: Abschnitt 4 und 5 enthalten die beiden Fingerabdrücke, die man sich merken sollte.

2. Die irreführendste Fehlermeldung in AES

Diese Messung ordnet bei den meisten die Debugging-Prioritäten neu. Schlüssel 0123456789abcdef, IV aus lauter Nullen, AES/CBC/PKCS5Padding, Klartext hello world, auf java 1.8.0_162 mit dem im JDK enthaltenen SunJCE-Provider:

SzenarioÄnderungGemessenes Ergebnis
ASchlüssel um 1 Byte falsch (letztes Zeichen fX)wirft javax.crypto.BadPaddingException: Given final block not properly padded. Das Padding selbst war nie fehlerhaft; die Meldung ist völlig irreführend
BSchlüssel korrekt, IV um 1 Byte falschkeine Exception, der Klartext hello world kam als iello world zurück. Nur das entsprechende Byte des ersten Blocks wurde beschädigt
CSchlüssel korrekt, den CBC-Chiffretext mit AES/ECB entschlüsseltstill erfolgreich, keine Exception. Eine Modus-Verwechslung muss nichts auslösen

Szenario A kostet ganze Nachmittage. Szenario C bringt kaputte Daten in die Produktion.

Warum ein falscher Schlüssel einen Padding-Fehler erzeugt

Am Padding war nichts falsch. Die verschlüsselnde Seite hat fünf 0x05-Bytes angehängt, um hello world auf sechzehn zu bringen, hat diesen Block verschlüsselt, und er liegt unbeschädigt in Ihrem Chiffretext.

Der Fehler passiert auf dem Rückweg. Die CBC-Entschlüsselung lässt die Blockchiffre rückwärts laufen, verknüpft jedes Ergebnis per XOR mit dem vorherigen Chiffretextblock und liest erst dann das Ende des letzten Blocks, um zu entscheiden, wie viele Bytes abzuschneiden sind. Mit dem falschen Schlüssel produziert die Chiffre sechzehn Bytes Rauschen, und Rauschen endet so gut wie nie auf einem gültigen PKCS#7-Muster. Die Bibliothek meldet, was sie gesehen hat, nämlich fehlerhaftes Padding, was zutrifft und nichts nützt.

Lesen Sie BadPaddingException als „der Klartext, den ich rekonstruiert habe, endet nicht so, wie aufgefüllter Klartext endet“. Der wahrscheinlichste Grund dafür, dass Ihre Rekonstruktion falsch ist, ist der Schlüssel. Deshalb landen eine Suche nach aes decrypt wrong key und eine Suche nach einer Bad-Padding-Exception in denselben Threads: Die beiden Symptome sind ein einziges Symptom. Noch ein Hinweis zum Design: Geben Sie diese Unterscheidung niemals an einen Aufrufer weiter, denn „Padding ungültig“ von „Padding gültig, Inhalt falsch“ unterscheiden zu können, ist genau das, wovon sich ein Padding-Oracle-Angriff ernährt (Vaudenay, EUROCRYPT 2002).

Was GCM anders macht

GCM dreht die Reihenfolge um und prüft den Tag, bevor überhaupt Klartext entsteht. Es gibt also kein Zeitfenster, in dem teilweise korrekte Bytes existieren. Nach einem GCM-Fehlschlag fragen Sie sich nie, ob die Ausgabe echt ist, denn es gibt keine Ausgabe. GCM hat außerdem überhaupt kein Padding, weil darunter ein Counter-Modus arbeitet, also entspricht die Chiffretextlänge der Klartextlänge. Ein Padding-Fehler in einem System, das Sie für GCM hielten, beweist damit, dass das System kein GCM ist, meist eine Konfiguration, die auf CBC zurückgefallen ist.

3. Verwenden beide Seiten dieselben Schlüsselbytes?

AES sieht Ihre Schlüsselzeichenkette nicht. Es sieht 16, 24 oder 32 Bytes. Zwei Systeme können identisches Schlüsselmaterial in einer Konfigurationsdatei stehen haben und sich trotzdem uneinig sein, denn „identisch“ ist eine Eigenschaft des Texts, nicht der Bytes.

Die drei Wege, auf denen aus einer Schlüsselzeichenkette Bytes werden

Übergeben Sie die wörtliche Zeichenkette 0123456789abcdef an drei verschiedene Bibliotheken:

als Hex         -> 8 Bytes    (ungültige AES-Schlüssellänge)
als Base64      -> 12 Bytes   (ungültige AES-Schlüssellänge)
als rohes UTF-8 -> 16 Bytes   (gültig, AES-128)

Sechzehn Zeichen, drei Byte-Anzahlen. Der Fall ist genau deshalb böse, weil er unter allen drei Lesarten gültig ist: Jedes Zeichen kommt sowohl im Hex- als auch im Base64-Alphabet vor, und sechzehn Zeichen sind für beide Dekodierer eine zulässige Länge, beim Parsen schlägt also nichts fehl.

Der Leitfaden zu „invalid signature“ bei JWT enthält die vollständige bibliotheksübergreifende Matrix dazu, wie jedes Ökosystem eine Secret-Zeichenkette interpretiert. Die Kurzfassung für AES lautet: Schreiben Sie auf, in welcher Kodierung Ihr Schlüsselmaterial vorliegt, und lassen Sie beide Seiten explizit dekodieren. Die HMAC-Variante desselben Fehlers erwischt Webhook-Empfänger, behandelt im Leitfaden zur fehlgeschlagenen Webhook-Signaturprüfung.

AES ist streng: genau 16, 24 oder 32 Bytes

Hier unterscheidet sich AES von dem Primitiv, dem die meisten Entwickler zuerst begegnen. HMAC akzeptiert jede Schlüssellänge: RFC 2104 hasht alles, was länger als die Blockgröße ist, und füllt alles Kürzere mit Nullen auf. Ein HMAC-Generator nimmt daher ein 7 Byte oder 700 Byte langes Secret klaglos entgegen. AES hat genau drei zulässige Schlüssellängen und weist alles andere zurück, bevor es einen einzigen Block anfasst.

Diese Strenge hilft Ihnen, denn ein Längenfehler ist der eine AES-Fehler, der seine eigene Ursache benennt, statt sich hinter dem Padding zu verstecken. Unser Tool formuliert ihn als Key must be 16, 24, or 32 bytes (AES-128/192/256). Die Fallen, die eine falsche Länge erzeugen:

  • Ein abschließender Zeilenumbruch aus KEY=$(cat key.txt) oder echo "$KEY". Verwenden Sie printf und echo -n. Ein aus der Oberfläche eines Secrets-Managers mitkopiertes Leerzeichen am Ende tut dasselbe.
  • Ein 0x-Präfix, aus einem Debugger kopiert: vierunddreißig Zeichen, die kein gültiges Hex mehr sind.
  • Nicht-ASCII-Zeichen. contraseña besteht aus 10 Zeichen und in UTF-8 aus 11 Bytes. Eine „32 Zeichen lange“ Passphrase mit einem einzigen Akzentbuchstaben hat also 33 Bytes.

SecretKeySpec und der Standardzeichensatz der Plattform

Java hat davon eine Variante, die erst nach dem Deployment auftritt. "my secret".getBytes() ohne Argument verwendet den Standardzeichensatz der Plattform, der vor JDK 18 aus der Property file.encoding und damit aus Betriebssystem und Locale der Maschine stammte. Ein Laptop mit UTF-8 und ein Container mit ANSI_X3.4-1968 erzeugen für jedes Nicht-ASCII-Zeichen unterschiedliche Bytes. JEP 400 hat UTF-8 in JDK 18 zum Standard gemacht, was neuen Code repariert und sonst nichts.

// falsch: die Bytes hängen von der Maschine ab
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(), "AES");

// richtig: die Bytes hängen von nichts ab
SecretKeySpec ks = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "AES");

Wenn Ihr Code lokal funktioniert, auf dem Server mit einem Padding-Fehler scheitert und die Passphrase irgendetwas außerhalb von ASCII enthält, prüfen Sie das zuerst.

4. Der IV: wohin er gehört und wie ein falscher aussieht

Ein aes iv mismatch ist der Fehler, den man zuerst vermutet und zuletzt diagnostiziert, weil er sich nicht wie die anderen verhält. Er ist leise, und er bleibt lokal begrenzt.

Ein falscher IV beschädigt genau einen Block

Sehen Sie sich Szenario B noch einmal an. Schlüssel korrekt, IV um ein Byte falsch:

hello world   ->   iello world

Keine Exception, ein einziges Zeichen. Schreiben Sie den CBC-Schritt für den ersten Block auf, dann ist es offensichtlich: P1 = D(C1) XOR IV. Der IV geht direkt per XOR in den ersten Klartextblock ein und berührt sonst nichts. Kippt man ein Bit des IV, kippt dasselbe Bit des Klartexts an derselben Stelle. Hier wurde aus h (0x68) ein i (0x69), das erste Byte des IV hat sich also um genau 0x01 verschoben.

Der Fingerabdruck. In CBC bedeutet „die ersten 16 Bytes Müll, alles danach sauber“, dass der IV falsch und der Schlüssel richtig ist. Jeder Block Müll bedeutet, dass der Schlüssel falsch ist. Diese eine Beobachtung trennt die beiden häufigsten Ursachen, ohne dass Sie eine Zeile Code ändern, und das AES-Entschlüsselungstool zeigt die dekodierten Bytes an, sodass Sie es direkt ablesen können.

Eine Exception gab es trotzdem nicht, und der Grund lässt sich nachrechnen. hello world ist 11 Bytes lang, also ein einziger Block, und das PKCS#7-Padding liegt darin in den Bytes 11 bis 15. Das veränderte IV-Byte war Byte 0, der Padding-Bereich blieb also unberührt und ging als gültig durch. Beschädigen Sie ein IV-Byte an Position 11 oder später, bekommen Sie stattdessen einen Padding-Fehler, ein weiterer Weg, auf dem die Padding-Meldung Sie anlügt.

Drei Übertragungskonventionen

Es gibt keinen Standard dafür, wohin der IV gehört, nur drei Gewohnheiten, die schlecht zusammenspielen.

Am häufigsten steht der IV vorn, als iv || ciphertext, so auch als Vorgabe in unseren Tools. Beide Seiten müssen sich einig sein, wie viel abzuschneiden ist: 16 Bytes bei CBC und CTR, 12 bei GCM. Der spiegelbildliche Fehler ist ein Sender, der voranstellt, und ein Empfänger, der das nicht tut. Die ersten 16 Bytes des „Chiffretexts“ sind dann der IV, jeder Block verschiebt sich, und Sie bekommen einen Padding-Fehler.

Die zweite Variante gibt dem IV ein eigenes Feld. {"iv": "...", "ciphertext": "..."} ist im Prinzip sauberer und verdoppelt die Stellen, an denen eine Kodierung abweichen kann, denn der IV hat jetzt seine eigene Base64-oder-Hex-Frage.

Bleibt die dritte Variante, ein fest verdrahteter Wert, meist lauter Nullen, weil jemand Determinismus brauchte. Sie funktioniert systemübergreifend perfekt, und genau das macht sie zur gefährlichen Variante: In CBC verrät ein fester IV Gleichheit zwischen Datensätzen, und in GCM legt die Wiederverwendung einer Nonce unter einem Schlüssel das XOR der beiden Klartexte offen und kann den GHASH-Unterschlüssel preisgeben, der den Tag authentifiziert. SP 800-38D ist bei der Eindeutigkeit unmissverständlich.

Mit dem Schalter für rohen Chiffretext plus einer expliziten IV-Vorgabe prüft das Tool alle drei Konventionen in einer Minute gegen dieselben Bytes.

Der IV von GCM hat 12 Bytes, nicht 16

Teams, die GCM einführen, indem sie einen bestehenden CBC-Pfad umbauen, schleppen den 16-Byte-IV mit, und das Ergebnis scheitert ohne jeden Hinweis.

SP 800-38D standardisiert einen 96-Bit-IV. Andere Längen sind erlaubt, aber sie sind nicht einfach „ein längerer IV“: Ist der IV nicht 96 Bit lang, leitet GCM seinen initialen Zählerblock ab, indem es den IV durch GHASH schickt, statt ihn direkt zu verwenden. Dieselben 16 Bytes als Nonce erzeugen daher einen völlig anderen Schlüsselstrom und Tag als die ersten 12, und Sie bekommen einen generischen Authentifizierungsfehler. Stammt der Chiffretext von woanders und Sie raten am Layout, zählen Sie rückwärts: Der Tag sind die letzten 16 Bytes, die Nonce fast immer die ersten 12.

5. Modus-Verwechslung, auch die stille Variante

Cipher.getInstance("AES") ist ECB

Java erlaubt es Ihnen, eine Chiffre zu benennen, ohne Modus oder Padding-Verfahren zu nennen. Es lehnt das nicht ab und warnt auch nicht. Unter dem im JDK enthaltenen SunJCE-Provider füllt es die Lücken mit ECB und PKCS5Padding.

Der Nachweis braucht das richtige Experiment: Verschlüsseln Sie 32 identische Bytes (zwei Blöcke aus A) mit dem Schlüssel 0123456789abcdef und prüfen Sie dann, ob die beiden Chiffretextblöcke übereinstimmen. Auf java 1.8.0_162:

getInstance("AES")           ciphertext = 3bfd04cc0d7ed55358e2cbe19de213833bfd04cc0d7ed55358e2cbe19de21383377222e061a924c591cd9c27ea163ed4
  block1 = 3bfd04cc0d7ed55358e2cbe19de21383
  block2 = 3bfd04cc0d7ed55358e2cbe19de21383   <- identische Blöcke = der ECB-Fingerabdruck (Klartextstruktur wird sichtbar)
getInstance("AES/CBC/PKCS5Padding")  Blöcke unterscheiden sich = die Verkettung ist aktiv

Byte für Byte identisch. Das ist die ECB-Signatur, dieselbe Eigenschaft, die dafür sorgt, dass der berühmte verschlüsselte Pinguin immer noch wie ein Pinguin aussieht. Das Experiment funktioniert nur mit identischen Klartextblöcken: Sechzehn A-Bytes gefolgt von sechzehn B-Bytes ergeben auch unter ECB zwei verschiedene Chiffretextblöcke, und Sie würden fälschlich schließen, die Vorgabe sei CBC.

Grenzen Sie das Ergebnis sauber ein: Es beschreibt den im JDK enthaltenen SunJCE-Provider in der oben genannten Version. Die Standard-Transformation ist eine Entscheidung des Providers, ein Fremdanbieter wie BouncyCastle kann dieselbe Kurzform also anders auflösen. Die Verallgemeinerung lautet nicht „Java bedeutet ECB“, sondern „eine unqualifizierte Transformationszeichenkette bedeutet, was immer Ihr Provider entscheidet, und genau deshalb schreibt man nie eine“.

Der falsche Modus muss keinen Fehler auslösen

Szenario C hat CBC-Chiffretext mit AES/ECB entschlüsselt und den korrekten Klartext ohne Exception zurückgegeben. Das wirkt unmöglich, bis man die Rechnung aufschreibt. Die CBC-Verschlüsselung des ersten Blocks ist C1 = E(P1 XOR IV), und die ECB-Entschlüsselung dieses Blocks ist D(C1) = P1 XOR IV. Der IV bestand hier aus lauter Nullen, also gilt P1 XOR 0 = P1 und der erste Block entschlüsselt sich einwandfrei. hello world ist einen Block lang, „der erste Block“ war also die ganze Nachricht.

Die allgemeine Regel: bei einem Null-IV stimmen ECB und CBC im ersten Block überein und weichen in jedem Block danach voneinander ab. Entschlüsseln Sie eine lange CBC-Nachricht als ECB, bekommen Sie sechzehn saubere Bytes gefolgt von Rauschen, die exakte Umkehrung des Fingerabdrucks für einen falschen IV. Zwei gegensätzliche Formen für zwei verschiedene Fehler, und in beiden Fällen schweigt die Bibliothek. Fest verdrahtete Null-IVs sind häufig genug, dass das keine Laborkuriosität ist.

Was Ihnen ein minimaler Aufruf in der jeweiligen Sprache liefert

ÖkosystemMinimaler AufrufModus, den Sie tatsächlich bekommen
Java (SunJCE)Cipher.getInstance("AES")ECB mit PKCS5Padding, stillschweigend
Node cryptocreateDecipheriv('aes-256-cbc', key, iv)was auch immer die Algorithmus-Zeichenkette sagt; eine Vorgabe existiert nicht
Web Cryptocrypto.subtle.decrypt({ name: 'AES-CBC', iv }, ...)explizit benannt; ECB ist überhaupt nicht implementiert
Python cryptographyCipher(algorithms.AES(key), modes.CBC(iv))das Modus-Objekt ist Pflicht
PyCryptodomeAES.new(key, AES.MODE_ECB)Pflichtargument, aber ECB steht direkt in der Autovervollständigung
Go crypto/aesaes.NewCipher(key) liefert einen rohen cipher.BlockDecrypt auf diesem Block aufzurufen ist ECB; umhüllen Sie ihn mit cipher.NewCBCDecrypter oder cipher.NewGCM
CryptoJSCryptoJS.AES.decrypt(ct, "passphrase")CBC, PKCS#7, EVP_BytesToKey mit MD5 (siehe Abschnitt 7)

Ökosysteme, in denen der Modus in einer Zeichenkette oder einem Objekt steckt, überraschen Sie nie. Die beiden, die einen Aufruf nach dem Motto „einfach AES“ anbieten, Java und Go, sind die Quelle der Meldungen über versehentliches ECB. Welcher Modus tatsächlich gelaufen ist, klärt sich schneller durch Ausprobieren als durch Codelesen: Lassen Sie dieselben Bytes gegen jeden Modus laufen.

6. GCM: dieselben Bytes, andere APIs

Die meisten sprachübergreifenden Fehlschläge rund um aes gcm auth tag sind nicht kryptografischer Natur. Beide Seiten haben dieselben 16 Bytes berechnet und sind sich uneinig, wo diese Bytes liegen.

Die Messung

Schlüssel = 32 Bytes 0123456789abcdef0123456789abcdef, IV = 12 Null-Bytes, Klartext hello world, auf node v25.8.2 und java 1.8.0_162:

Node   ciphertext = a616cd6d7d2328379d41e5                    (11 B)   <- update+final
       authTag    = c87af9f8ad7148e873fa797292c0af3f          (16 B)   <- separat über getAuthTag() abgeholt
Java   doFinal()  = a616cd6d7d2328379d41e5c87af9f8ad7148e873fa797292c0af3f   (27 B)   <- Chiffretext und Tag bereits aneinandergehängt

Node ciphertext || authTag ist exakt Java doFinal(), alle 27 Bytes davon. Kein Kodierungsunterschied, nichts auszuhandeln: Node reicht Ihnen die beiden Teile getrennt, Java reicht sie zusammengeklebt heraus. 11 Bytes Klartext ergaben außerdem 11 Bytes Chiffretext, weil GCM kein Padding hinzufügt. Deshalb kann ein Padding-Fehler niemals aus einem echten GCM-Pfad stammen.

Aneinandergehängt oder getrennt, nach Laufzeitumgebung

LaufzeitumgebungVerschlüsselungs-APIWo der Tag landet
Node cryptoupdate() + final(), dann getAuthTag()getrennt
Java (SunJCE, AES/GCM/NoPadding)doFinal()angehängt
Go cipher.AEADSeal()angehängt
Python cryptography, AESGCMencrypt()angehängt
Python cryptography, Cipher + modes.GCMfinalize(), dann encryptor.taggetrennt
Web Cryptocrypto.subtle.encryptangehängt

Node ist unter den High-Level-APIs der Ausreißer, weshalb „Node zu irgendetwas“ die am häufigsten gemeldete Fehlerrichtung ist. So packen Sie die Node-Ausgabe für einen Java-, Go-, Python- oder Browser-Empfänger:

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()]);   // passt jetzt zu doFinal()

So entpacken Sie einen zusammengehängten Blob für Node:

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

Die Reihenfolge ist eine echte Bedingung: Rufen Sie setAuthTag() nach final() auf, wirft Node Unsupported state or unable to authenticate data, selbst wenn jedes Byte korrekt ist. Bleibt unklar, wo der Tag in Ihrem Blob sitzt, können Sie mit denselben Bytes die Tag-Position gegenprüfen, statt sie aus der Länge zu erraten.

Die Tag-Länge ist variabel, die Einheit nicht

GCM erlaubt Tags von 128, 120, 112, 104 oder 96 Bit, wobei 64 und 32 für eingeschränkte Anwendungen reserviert sind (SP 800-38D, Anhang C). Fast alle verwenden 128, und der Ärger liegt darin, wie jede API danach fragt:

  • Java: new GCMParameterSpec(128, iv). Das erste Argument sind Bit.
  • Web Crypto: { name: 'AES-GCM', iv, tagLength: 128 }. Ebenfalls Bit, Vorgabe 128.
  • Node: createCipheriv(algo, key, iv, { authTagLength: 16 }). Hier sind es Bytes.

new GCMParameterSpec(16, iv) ist eine legal aussehende Java-Zeile, die einen 16-Bit-Tag anfordert. Manche JDKs weisen sie zurück, und wo sie akzeptiert wird, haben Sie Ihre Integritätsgarantie gegen einen Münzwurf mit einer Chance von eins zu 65.536 eingetauscht. Sind sich beide Seiten über die Tag-Länge uneinig, unterscheiden sich auch die gepackten Längen: Der Empfänger schneidet an der falschen Grenze und bekommt einen Authentifizierungsfehler, der nichts mit dem Schlüssel zu tun hat. Der Begriff Tag-Länge ist dabei selbst ein Kompromiss: Padding, Tag und Mode übersetzt in deutscher Doku kaum jemand, während derselbe Vorgang mal Entschlüsselung, mal Dechiffrierung heißt und die Eingabe mal Chiffretext, mal Geheimtext. Weil die Fehlermeldung ohnehin nur auf Englisch erscheint, bleibt jede deutschsprachige Suche danach halb englisch.

7. Sie haben eine Passphrase, keinen Schlüssel

Wenn eine der beiden Seiten eine von Menschen getippte Zeichenkette entgegennimmt, liegt zwischen dieser Zeichenkette und AES eine Schlüsselableitungsfunktion, und eine Abweichung beim KDF ist unsichtbar. Das KDF wirft nie einen Fehler. Es liefert 32 vollkommen einwandfreie Bytes, die zufällig die falschen 32 Bytes sind, und eine Schicht tiefer sehen Sie dann wieder einen Padding-Fehler.

Bei PBKDF2 müssen vier Dinge zusammenpassen

  • Das Salt. Im OpenSSL-Format Salted__ sind es 8 Bytes innerhalb des Chiffretexts; im Passphrasen-Format unserer Tools ist es ein 16 Byte langes Präfix; in selbstgebauten Verfahren ist es häufig eine fest verdrahtete Konstante.
  • Die Iterationszahl. openssl enc -pbkdf2 verwendet standardmäßig 10.000. OWASP empfiehlt derzeit 600.000 für PBKDF2-HMAC-SHA256, und genau das nutzt unser Passphrasen-Modus. Frameworks wählen ihre eigenen Zahlen.
  • Der Hash. SHA-1 gegen SHA-256 gegen SHA-512. Älterer Code und manche Mobile-SDKs setzen weiterhin standardmäßig auf SHA-1.
  • Die Ausgabelänge. Zweiunddreißig Bytes für AES-256, sechzehn für AES-128. Manche Verfahren leiten Schlüssel und IV gemeinsam aus einem einzigen längeren Aufruf ab, was nie zu einer schlichten 32-Byte-Ableitung passt.

EVP_BytesToKey, und warum CryptoJS immer wieder nicht funktioniert

Hinter cryptojs aes decrypt not working steckt meist eine ganz bestimmte Abweichung. CryptoJS.AES.encrypt(text, "passphrase") verwendet kein PBKDF2. Es verwendet EVP_BytesToKey, die Ableitung von OpenSSL vor 1.1, mit MD5 und einer einzigen Iteration.

EVP_BytesToKey tut außerdem etwas, was PBKDF2 nicht tut: Es leitet in einem Durchgang den Schlüssel und den IV aus Passphrase und Salt ab. Deshalb trägt eine OpenSSL-Salted__-Datei kein eigenes IV-Feld, und deshalb ist es gleich doppelt falsch, CryptoJS-Ausgaben mit PBKDF2 plus einem zufälligen IV nachbauen zu wollen.

Das Format erkennt man auf einen Blick: Die 8 ASCII-Bytes Salted__, gefolgt von einem 8 Byte langen Salt, beginnen Base64-kodiert immer mit U2FsdGVkX1. Fängt Ihr Chiffretext so an, ist er aus einer Passphrase abgeleitet und Sie müssen wissen, aus welcher Ableitung. Das AES-Entschlüsselungstool erkennt das Präfix und wechselt ohne Codeänderung zwischen den dreien.

Warum dasselbe Passwort unterschiedliche Schlüssel ergibt

So etwas wie „das AES-Passwort“ gibt es nicht. Jede Bibliothek hat ihren eigenen Weg von der Zeichenkette zum Schlüssel erfunden:

ErzeugerAbleitungErgebnis für eine Passphrase
CryptoJS AES.encrypt(text, pass)EVP_BytesToKey, MD5, 1 IterationSchlüssel A
openssl enc 1.0.2 und älterEVP_BytesToKey, MD5, 1 IterationSchlüssel A
openssl enc 1.1+ ohne -pbkdf2EVP_BytesToKey, SHA-256, 1 IterationSchlüssel B
openssl enc -pbkdf2PBKDF2-HMAC-SHA256, 10.000 IterationenSchlüssel C
Unser Passphrasen-ModusPBKDF2-HMAC-SHA256, 600.000 IterationenSchlüssel D
Java, Python, Goüberhaupt keine Vorgabe; Sie schreiben die Ableitung selbstwas auch immer Sie geschrieben haben

Vier Schlüssel aus einem Passwort, bevor überhaupt jemand einen Fehler gemacht hat. Das Upgrade von 1.0.2 auf 1.1 hat den Standard-Digest von MD5 auf SHA-256 geändert, weshalb sich Chiffretext aus alten Skripten auf einer neueren Maschine mit demselben Befehl plötzlich nicht mehr entschlüsseln ließ. Haben Sie Daten geerbt und niemand erinnert sich an die Toolchain, probieren Sie die Ableitungen in dieser Reihenfolge durch. Nach spätestens drei Versuchen wissen Sie es.

8. Was der Transport mit Ihren Bytes gemacht hat

Chiffretext ist gleichverteilt zufälliges Binärmaterial, was ihn maximal feindselig gegenüber allem macht, was Bytes als Text behandelt. Ein großer Teil der AES-Fehlschläge hat mit der Chiffre selbst überhaupt nichts zu tun.

Base64-Varianten und fehlendes Padding

Standard-Base64 (RFC 4648 §4) verwendet + und /; die URL-sichere Variante (§5) verwendet - und _. Geben Sie eine URL-sichere Zeichenkette an einen Standard-Dekodierer, wirft er entweder einen Fehler oder verwirft in nachsichtigen Implementierungen die störenden Zeichen stillschweigend und liefert zu kurze, falsch ausgerichtete Bytes zurück. Deshalb liefert Java Base64.getUrlDecoder() und Base64.getDecoder() als getrennte Objekte aus. Manche Kodierer lassen außerdem das abschließende = weg, manche Dekodierer bestehen darauf, und Codepfade im Umfeld von JWT entfernen es standardmäßig.

Bevor Sie den Schlüssel verdächtigen, dekodieren Sie den Chiffretext und prüfen Sie seine Länge gegen den Modus:

  • CBC und ECB: ein Vielfaches von 16 ungleich null. Alles andere ist eine Kürzung oder ein Dekodierproblem, kein Schlüsselproblem.
  • GCM: Die Chiffretextlänge entspricht der Klartextlänge, plus 16 für den Tag, plus 12 am Anfang, falls die Nonce vorangestellt ist.
  • CTR: jede beliebige Länge, diese Prüfung sagt Ihnen hier also nichts.

Der Base64-Dekodierer liefert Ihnen die Byte-Anzahl mit einem einzigen Einfügen, oft die schnellste Messung der ganzen Untersuchung.

Zeilenumbrüche, typografische Anführungszeichen und der UTF-8-Rundlauf

openssl base64 bricht die Ausgabe bei 64 Spalten um, sofern Sie nicht -A übergeben, und manche Dekodierer überspringen eingebettete Zeilenumbrüche, während andere sie zurückweisen. Dieselbe Datei dekodiert also auf einer Maschine und scheitert auf der nächsten. Wer über einen Chat-Client oder einen Dokumenteneditor kopiert, macht aus geraden Anführungszeichen typografische und aus Bindestrichen Halbgeviertstriche, und im Terminal ist der Unterschied kaum zu sehen.

Nicht wiederherstellbar ist der UTF-8-Rundlauf. Hält irgendeine Schicht rohe AES-Ausgabe als Zeichenkette, ohne sie vorher zu kodieren (new String(cipherBytes) in Java, bytes.decode('utf-8', errors='replace') in Python, irgendwo ein TextDecoder), fällt jede Byte-Folge, die kein gültiges UTF-8 ist, auf U+FFFD zusammen, und beim Zurückkodieren steht EF BF BD dort, wo einmal Ihre Daten waren. Da etwa die Hälfte zufälliger Bytes nicht ASCII ist, trifft das den größten Teil des Chiffretexts, und kein Schlüssel holt ihn zurück. Der Encoding-Guide zu UTF-8 und UTF-16 erklärt, warum der Verlust eine Einbahnstraße ist. Binärer Chiffretext reist als Base64, als Hex oder als Binärdaten, aber nie als Zeichenkette.

Datenbankspalten

Beim Speichern passiert derselbe Schaden, nur noch leiser. Schreiben Sie Chiffretext in ein VARCHAR(255) und er ist einen Block zu lang, schneidet MySQL ihn ab, außerhalb des Strict Mode ohne jede Fehlermeldung. Am Ende sitzen der Padding-Block und der GCM-Tag, eine vor Monaten „erfolgreich“ geschriebene Zeile scheitert also jetzt, und wenn der Schnitt auf eine 16-Byte-Grenze fiel, greift auch die obige Längenprüfung nicht. Die Zeichensatzkonvertierung erledigt den Rest: Eine latin1-Spalte, die UTF-8-Bytes bekommt, schreibt Ihre Daten schon beim Hineinschreiben um.

Speichern Sie Chiffretext in VARBINARY, BLOB oder bytea, oder legen Sie Base64 in einer Textspalte mit reichlich Platz ab.

9. Ein Bisektions-Workflow, der es in fünf Minuten findet

Jeder Abschnitt oben grenzt eine Variable ein. Wenn Sie diese Variablen der Reihe nach gegen eine Referenzimplementierung laufen lassen, die Sie kontrollieren, konvergiert das schnell. Die Browser-Tools eignen sich gut als diese Referenz, weil Sie eine Einstellung nach der anderen ändern und die Bytes dabei sehen können; sie laufen vollständig in Ihrem Browser und schicken nichts an einen Server, Schlüssel und Chiffretext verlassen die Seite also nie.

  1. Schritt 0: die Form messen. Dekodieren Sie den Chiffretext und notieren Sie die Byte-Anzahl, die ersten paar Bytes und ob er mit U2FsdGVkX1 beginnt. Prüfen Sie die Anzahl gegen Abschnitt 8. Ist sie kein Vielfaches von 16 und Sie glauben, in CBC zu sein, hören Sie auf: Das ist ein Transportfehler.

  2. Schritt 1: einen bekannten Klartext verschlüsseln. Verschlüsseln Sie im AES-Verschlüsselungstool eine kurze, bekannte Zeichenkette mit den Parametern, von denen Sie glauben, dass die Produktion sie verwendet, und vergleichen Sie dann die Form der beiden Ausgaben statt ihrer Werte: Gesamtlänge, Präfix-Bytes, Vorhandensein eines Salt-Headers. Weichen sie ab, ist Ihre Annahme über das Format oder das KDF falsch, und kein noch so langes Herumprobieren am Schlüssel repariert das.

  3. Schritt 2: die Ableitungen durchgehen. Führen Sie bei aus einer Passphrase abgeleiteten Daten im AES-Entschlüsselungstool PBKDF2 mit der exakten Iterationszahl aus, dann EVP-SHA256, dann EVP-MD5. Genau eine davon kann richtig sein. Funktioniert keine, liegt der Fehler oberhalb des KDF.

  4. Schritt 3: jede Konvention entfernen. Wechseln Sie auf einen rohen Schlüssel, schalten Sie rohen Chiffretext ein und geben Sie den IV explizit an. Sie legen damit genau fest, welche Bytes Schlüssel, IV und Chiffretext sind, und überlassen der Bibliothek nichts zum Raten. Entschlüsselt es hier, aber nicht in Ihrem Code, ist Ihr Fehler ein Rahmungsfehler (ein nicht abgeschnittenes IV-Präfix, ein Tag an der falschen Stelle) und kein kryptografischer.

  5. Schritt 4: den Modus wechseln. Probieren Sie CBC, dann CTR, dann GCM gegen dieselben Bytes. Liefert CTR lesbaren Text, wo CBC scheiterte, steht die Modus-Verwechslung fest.

  6. Schritt 5: den Müll lesen. Erster Block kaputt und der Rest sauber bedeutet den IV. Erster Block sauber und der Rest kaputt bedeutet, dass Sie CBC als ECB mit einem Null-IV entschlüsselt haben. Alles kaputt bedeutet den Schlüssel oder die Ableitung.

10. Häufige Fragen

Warum funktioniert mein AES-Code lokal, scheitert aber in der Produktion?

Zwischen lokal und Produktion hat die Umgebung etwas verändert, das nicht in der Versionsverwaltung steht. Die üblichen Verdächtigen, der Reihe nach: Der Schlüssel kam aus einer Umgebungsvariablen oder einem Secrets-Manager mit einem abschließenden Zeilenumbruch; Javas Standardzeichensatz der Plattform unterscheidet sich zwischen Laptop und Container, getBytes() hat also andere Bytes erzeugt (Abschnitt 3); in der Produktion läuft OpenSSL 1.1+, während Ihre lokalen Skripte auf 1.0.2 zielten, was den EVP_BytesToKey-Digest von MD5 auf SHA-256 umstellt; oder eine Datenbankspalte schneidet den Chiffretext nur in einer Umgebung ab. Geben Sie zuerst auf beiden Seiten Schlüssellänge und Chiffretextlänge in Bytes aus, denn diese beiden Zahlen klären es meistens.

Ich habe in Node verschlüsselt und kann in Java nicht entschlüsseln. Wo fange ich an?

Von Node nach Java fangen Sie beim GCM-Tag an, der häufigsten und am wenigsten offensichtlichen Ursache. Node gibt Chiffretext und Tag getrennt zurück; Javas doFinal() erwartet sie als ciphertext || tag aneinandergehängt, und Abschnitt 6 zeigt, dass die Bytes ansonsten identisch sind. Sind Sie stattdessen in CBC, beginnen Sie bei der IV-Konvention: Hat Node ihn vorangestellt, und schneidet die Java-Seite vor dem Entschlüsseln 16 Bytes ab? An dritter Stelle steht der Schlüssel selbst, bei dem Buffer.from(k, 'hex') und k.getBytes(StandardCharsets.UTF_8) aus derselben Zeichenkette unterschiedliche Längen erzeugen.

Ist Javas PKCS5Padding dasselbe wie PKCS#7?

Für AES ist PKCS5Padding praktisch dasselbe wie PKCS#7, also ja. PKCS#5 (RFC 8018) ist nur für 8-Byte-Blöcke definiert; PKCS#7 (RFC 5652) verallgemeinert das Verfahren auf Blockgrößen von 1 bis 255 Bytes. Javas PKCS5Padding, angewandt auf eine Blockchiffre mit 16 Byte Blockgröße, implementiert das Verhalten von PKCS#7, und der Name ist ein historisches Überbleibsel. Das ist also nie Ihr Fehler. NoPadding schon: Es verlangt Klartext, der bereits ein Vielfaches von 16 ist, und bei der Entschlüsselung gibt es das Padding als Daten zurück, sodass Sie plausiblen Text mit angehängten Bytes wie \x05\x05\x05\x05\x05 sehen.

Mein Schlüssel hat 32 Zeichen, aber AES meldet eine ungültige Schlüssellänge. Warum?

Ein Längenfehler bedeutet, dass die Bibliothek eine Byte-Anzahl bekommen hat, die nicht 16, 24 oder 32 ist. Bei einer 32 Zeichen langen Zeichenkette ist das meist ein abschließender Zeilenumbruch (33 Bytes), ein 0x-Präfix, das die Zeichenkette zu ungültigem Hex macht, oder ein Nicht-ASCII-Zeichen, das in UTF-8 zwei oder drei Bytes belegt. Die gefährlichere Variante ist, überhaupt keinen Fehler zu bekommen: 32 Hex-Zeichen dekodieren zu 16 gültigen Bytes und 32 Base64-Zeichen zu 24 gültigen Bytes, beides zulässige AES-Längen. Die Bibliothek akzeptiert sie, verwendet den falschen Schlüssel und reicht Ihnen stattdessen einen Padding-Fehler. Prüfen Sie die Byte-Anzahl, nicht die Zeichenanzahl.

Die Entschlüsselung war „erfolgreich“, aber die Ausgabe ist Müll. Was ist schiefgelaufen?

Müll bei scheinbar erfolgreicher Entschlüsselung heißt, dass Sie in einem Modus sind, der nichts prüft. CTR und ECB werfen nie eine Exception, und CBC wirft nur, wenn das letzte Byte-Muster die Padding-Prüfung nicht besteht, was ein falscher Schlüssel in knapp unter 0,4 % der Fälle trotzdem schafft. Lesen Sie die Form: die ersten 16 Bytes kaputt und der Rest sauber bedeutet den IV; die ersten 16 sauber und der Rest kaputt bedeutet, dass Sie CBC-Chiffretext als ECB mit einem Null-IV entschlüsselt haben; durchgängig kaputt bedeutet den Schlüssel oder die Ableitung. Lesbarer Text mit ein paar seltsamen Bytes am Ende bedeutet NoPadding auf aufgefüllten Daten. Die langfristige Lösung heißt GCM, damit „erfolgreich“ auch etwas bedeutet.

Kann ich noch entschlüsseln, wenn ich den IV verloren habe?

Ohne den IV entschlüsseln Sie in CBC weiterhin alles außer den ersten 16 Bytes. Die Blöcke ab 2 werden als D(C_i) XOR C_{i-1} wiederhergestellt, und jede Eingabe dafür steckt bereits im Chiffretext, nur der erste Block braucht den IV. Wissen Sie zusätzlich, wie der Klartext beginnt, etwa ein JSON-Blob, das mit {"userId": anfängt, können Sie den IV direkt als D(C1) XOR P1 zurückrechnen. In CTR sät der IV den gesamten Schlüsselstrom, ihn zu verlieren heißt also, alles zu verlieren. In GCM speist die Nonce sowohl den Zähler als auch den Tag, eine teilweise Wiederherstellung gibt es dort nicht.

Kann ich den Klartext wiederherstellen, wenn der GCM-Tag abgeschnitten oder weggefallen ist?

Bei abgeschnittenem oder fehlendem GCM-Tag geht es mathematisch, praktisch mit Aufwand. Unter GCM steckt der CTR-Modus, Schlüssel und Nonce allein reproduzieren also den Schlüsselstrom. Keine verbreitete Bibliothek nimmt Ihnen das ab: Java, Go, Python und Web Crypto weigern sich allesamt bewusst, Klartext ohne gültigen Tag herauszugeben. Der Umweg besteht darin, dieselben Bytes als AES-CTR zu entschlüsseln, mit dem initialen Zählerblock aus der 12-Byte-Nonce gefolgt von 00000002, denn dort beginnt der erste Datenblock von GCM. Sie bekommen die Daten zurück und geben jede Integritätsgarantie auf, behandeln Sie das Ergebnis also als nicht vertrauenswürdig. Haben Sie noch alle 16 Tag-Bytes und die Authentifizierung scheitert trotzdem, fehlt der Tag nicht und irgendetwas anderes auf dieser Seite ist Ihr Fehler. Nehmen Sie ihn ins AES-Entschlüsselungstool mit und beginnen Sie bei Schritt 0.

Tags: aes encryption debugging cryptography interoperability

Verwandte Artikel

Alle Artikel anzeigen