Warum Base64 Padding braucht
Base64 macht aus 3 Bytes 4 Zeichen (der vollständige Algorithmus steht unter So funktioniert Base64). Ist die Eingabelänge kein Vielfaches von 3, ist der letzte Block verkürzt:
| Bytes im letzten Block | Datenzeichen | Padding | Beispiel |
|---|---|---|---|
| 3 | 4 | keins | Man → TWFu |
| 2 | 3 | = | Ma → TWE= |
| 1 | 2 | == | M → TQ== |
Das = hält die Ausgabelänge bei einem Vielfachen von 4. Das erleichterte Decodern aus der MIME-Ära die Arbeit: Sie konnten die Eingabe in festen Gruppen zu 4 Zeichen lesen und wussten, wo die Daten enden, ohne die Gesamtlänge mitzuzählen.
Streng genommen ist die Information redundant. Eine letzte Gruppe aus 3 Zeichen kann nur 2 Bytes bedeuten, eine Gruppe aus 2 Zeichen nur 1 Byte, ein Decoder, der Zeichen zählt, braucht also überhaupt kein =. Deshalb ist Base64 ohne Padding möglich, und deshalb nutzen viele Formate es.
In einem Fall trägt Padding echte Information: beim Aneinanderhängen. Werden zwei Zeichenketten mit Padding zusammengefügt, markiert das = in der Mitte, wo die erste endet. Decoder gehen damit allerdings schlecht um. TQ==TQ== (zwei kodierte M) lieferte mit Pythons Standard-Decoder b'M\x04\xd0', Nodes Buffer lieferte nur M, und atob warf einen Fehler. Dekodieren Sie jedes Stück stattdessen einzeln.
Wann Padding optional ist
RFC 4648 §3.2 sagt, dass Encoder Padding hinzufügen MÜSSEN, sofern die Spezifikation, die Base64 verwendet, nicht ausdrücklich etwas anderes festlegt. Mehrere tun das:
- JWT und JOSE (RFC 7515) verwenden Base64URL ohne Padding. Ein JWT-Header wie
eyJhbGciOiJIUzI1NiJ9hat kein=, auch wenn die Länge kein Vielfaches von 4 ist. Siehe Base64 in JWT. - URLs und Dateinamen transportieren oft Base64URL ohne Padding, weil
=in Query-Strings eine Bedeutung hat und als%3Dmaskiert werden müsste. Die Seite Base64 vs. Base64URL behandelt den Unterschied vollständig. - WebAuthn und OAuth PKCE (die
code_challengein RFC 7636) verwenden ebenfalls Base64URL ohne Padding.
Standard-Base64 in E-Mails, PEM-Dateien, Data-URIs und den meisten JSON-APIs hat Padding. Im Zweifel erzeugen Sie Standard-Base64 mit Padding und Base64URL ohne Padding und lassen Ihren Decoder beides akzeptieren.
Decoder, die Padding verlangen
Das Verhalten unterscheidet sich stärker, als man erwartet. Das sind die Ergebnisse beim Dekodieren von TWE (Ma ohne Padding) mit dem Standard-Decoder der jeweiligen Bibliothek:
| Sprache / Tool | Aufruf | Ergebnis für TWE |
|---|---|---|
| Python 3.13 | base64.b64decode | binascii.Error: Incorrect padding |
| Python 3.13 | base64.urlsafe_b64decode | ebenfalls Incorrect padding |
| .NET | Convert.FromBase64String | FormatException |
| Go | base64.StdEncoding | Fehler; für Eingaben ohne Padding RawStdEncoding nehmen |
| Go | base64.RawStdEncoding | Ma, lehnt aber TWE= ab |
| Ruby | Base64.strict_decode64 | ArgumentError: invalid base64 |
| Ruby | Base64.decode64, urlsafe_decode64 | Ma |
| Java 21 | Base64.getDecoder().decode | Ma |
| Node.js | Buffer.from(s, 'base64'), atob | Ma |
| PHP 8.4 | base64_decode (auch im strikten Modus) | Ma |
| GNU coreutils 9.7 | base64 -d | Ma |
| OpenSSL 3.5 | openssl base64 -d | leere Ausgabe, Exit-Code 0 |
Beachten Sie die OpenSSL-Zeile: Es gab nichts aus und meldete trotzdem Erfolg, ein Skript, das Base64 ohne Padding durch openssl base64 -d schickt, erzeugt also stillschweigend eine leere Datei. In denselben Tests lieferte es auch für eine einzelne Zeile ohne abschließenden Zeilenumbruch nichts; mit -A ließ sich dieser Fall beheben.
Go ist in der Gegenrichtung am strengsten: RawStdEncoding lehnt eine Zeichenkette ab, die Padding hat. Wenn Sie Eingaben aus mehreren Quellen annehmen, entfernen Sie = und nutzen die Raw-Kodierung, oder ergänzen Sie das Padding und nutzen die Standardkodierung.
Sprachspezifische Beispiele stehen auf den Code-Seiten, etwa Base64 in Python und Base64 in Go.
So stellen Sie fehlendes Padding wieder her
Die Regel hängt nur von der Länge der Zeichenkette nach dem Entfernen von Leerraum ab:
len % 4 | Was anhängen | Bedeutung |
|---|---|---|
| 0 | nichts | vollständige Blöcke |
| 2 | == | der letzte Block enthält 1 Byte |
| 3 | = | der letzte Block enthält 2 Bytes |
| 1 | nicht reparierbar | abgeschnitten oder ein überzähliges Zeichen |
Ein Rest von 1 ist nie gültig. Ein Base64-Zeichen trägt 6 Bit, das reicht nicht für ein Byte, und kein Padding der Welt macht das dekodierbar. Sehen Sie diesen Fall, suchen Sie nach einem fehlenden Zeichen, einem überzähligen Zeichen oder einem mitgezählten Zeilenumbruch.
Die Korrektur ist in den meisten Sprachen ein einziger Ausdruck:
import base64
s = "eyJzdWIiOiIxMjM0In0" # 19 chars, 19 % 4 == 3
padded = s + "=" * (-len(s) % 4) # adds one "="
print(base64.urlsafe_b64decode(padded))
# b'{"sub":"1234"}'
const s = "eyJzdWIiOiIxMjM0In0";
const padded = s + "=".repeat((4 - (s.length % 4)) % 4);
// "eyJzdWIiOiIxMjM0In0="
Der Base64-Validator meldet Länge und Padding-Zustand einer Zeichenkette, und Base64 reparieren ergänzt fehlende =, entfernt Leerraum und Anführungszeichen und wandelt URL-sichere Zeichen in einem Schritt um.
Nicht kanonische Kodierungen
Das Padding hat einen unauffälligeren Verwandten: die ungenutzten Bits im letzten Datenzeichen. Beim Kodieren von Ma füllen die 16 Bit zweieinhalb 6-Bit-Gruppen. Das dritte Zeichen trägt die letzten 4 Bit von a plus 2 Füllbits, und RFC 4648 §3.5 sagt, dass der Encoder diese Füllbits auf null setzen muss. Das ergibt E (000100).
Ein Decoder ignoriert die Füllbits, daher wird TWF= (wobei F 000101 ist, Füllbits 01) ebenfalls zu Ma dekodiert. Python, Node, Java und PHP akzeptierten in unseren Tests alle TWF= und lieferten Ma. Ebenso wird TR== genau wie TQ== zu M dekodiert.
Zeichenketten wie TWF= heißen nicht kanonisch. An einigen Stellen spielt das eine Rolle:
- Signaturen und Hashes über Base64-Text. Zwei verschiedene Zeichenketten ergeben dieselben Bytes, ein Vergleich von Base64-Zeichenketten, um die Gleichheit von Daten zu prüfen, kann also die falsche Antwort liefern. Vergleichen Sie die dekodierten Bytes.
- Deduplizierung und Cache-Schlüssel. Dasselbe Problem: Normalisieren Sie durch Dekodieren und erneutes Kodieren.
- Strenge Parser. RFC 4648 erlaubt Decodern, Füllbits ungleich null abzulehnen. Gos
StdEncoding.Strict()und GNUbase64 -dlehnenTWF=beide ab.
Überzähliges Padding ist eine verwandte Kuriosität. TWE== hat ein = zu viel. Python und Nodes Buffer akzeptierten es, atob warf InvalidCharacterError.
Padding beim Kodieren in Stücken
Wenn Sie eine große Datei stückweise in Base64 kodieren, muss jedes Stück außer dem letzten ein Vielfaches von 3 Bytes sein. Andernfalls bekommt jedes Stück sein eigenes Padding, und die zusammengefügte Ausgabe ist falsch. hello world!! in Stücken zu 4 Bytes kodiert ergab aGVsbA==byB3bw==cmxkIQ==IQ==; in Stücken zu 3 Bytes ergab es aGVsbG8gd29ybGQhIQ==, identisch mit der Kodierung des Ganzen auf einmal. Für den MIME-Zeilenumbruch sind 57 Bytes pro Zeile die natürliche Stückgröße, denn 57 Bytes werden zu genau 76 Zeichen.
Fügen Sie eine Zeichenkette mit fehlendem oder kaputtem Padding ein und erhalten Sie gültiges Base64 zurück.
Häufige Fragen
Kann ich die =-Zeichen einfach aus Base64 löschen?
Die Daten bleiben erhalten, denn die Länge der Zeichenkette verrät einem Decoder bereits, wie viele Bytes der letzte Block enthält. Ob das Ergebnis noch dekodiert wird, hängt vom Empfänger ab: Base64URL in JWTs erwartet kein Padding, Pythons b64decode und Convert.FromBase64String in .NET lehnen Zeichenketten ohne Padding dagegen ab.
Warum hat meine Base64-Zeichenkette kein = am Ende?
Entweder war die Eingabelänge ein Vielfaches von 3 Bytes, sodass kein Padding nötig war, oder der Erzeuger verwendet Base64 ohne Padding, was bei Base64URL in Tokens und URLs üblich ist.
Wie behebe ich den Fehler Incorrect padding in Python?
Hängen Sie vor dem Dekodieren '=' * (-len(s) % 4) an. Ist die Zeichenkette URL-sicher, nehmen Sie außerdem base64.urlsafe_b64decode. Ist die Länge mod 4 gleich 1, hilft Padding nicht: Die Zeichenkette ist abgeschnitten oder enthält ein überzähliges Zeichen.
Kann eine Base64-Zeichenkette mit drei =-Zeichen enden?
Nein. Ein letzter Block enthält 1 oder 2 übrig gebliebene Bytes, die == oder = ergeben. Drei = würden einen Block ohne Daten bedeuten, und den gibt es nicht.