Base64-Dekodierung in Python: Ein vollständiger Leitfaden
Irgendwo in Ihrem Code ist gerade ein String gelandet, der überhaupt nicht nach Text aussieht: eine lange Abfolge von A bis Z, ein paar Ziffern, das gelegentliche + oder /, vielleicht ein - oder _, und vielleicht ein oder zwei = Zeichen, die am Ende parken. Hinter diesem String könnte der Payload eines JWT stecken, das Ihr Gateway abgelehnt hat, ein Bild, das in einer HTML-Seite versteckt ist, eine Datei, die Ihnen jemand als .b64-Anhang per E-Mail geschickt hat, oder ein Zertifikatsblock in einem Ticket, das drei Helpdesks durchgereist ist. Ihre Aufgabe ist es, die ursprünglichen Bytes zurückzugeben, genau so, wie sie waren. Und Python ist für diesen Job in bester Laune, denn die komplette Werkzeugkiste steckt seit Jahrzehnten in der Standardbibliothek: eine Zeile import base64 und Sie sind auf jeder Plattform startklar, nichts zu installieren und nichts zu konfigurieren.
Kurz zur Wiederholung, während Sie es sich bequem machen, denn das braucht jeder einmal im Jahr: Base64 schreibt jeweils drei Bytes Daten in vier Zeichen um, die aus einem 64-Zeichen-Alphabet stammen, und wenn die letzte Gruppe von drei Bytes unvollständig ist, füllt =-Padding die Gruppe auf, damit die Ausgabe immer zu Vieren kommt. Das ist der ganze Trick. Es ist keine Kompression und keine Geheimhaltung, nur ein Weg, damit Binärdaten über Kanäle überleben, die nichts als Text annehmen. Die Startseite dieser Site geht das Format in voller Tiefe durch, Alphabet und Padding-Mathematik inklusive, also widmen wir unsere Energie dem Ort, wo der Schmerz wirklich sitzt: der Python-Seite des Dekodierens und dem, das Ergebnis ehrlich zu halten.
Drei Fakten formen alles, was folgt, und es lohnt sich, sie auswendig zu lernen, bevor Sie noch eine Zeile weiterlesen. Erstens hat der Decoder zwei Launen: einen höflichen, nachsichtigen Standard, der alles, was er nicht erkennt, stillschweigend verwirft, und einen strengen Modus, der solche Eingabe einfach ablehnt. Zweitens ist das Ergebnis eines Dekodierens immer ein bytes-Objekt, nie ein String, und der Moment, in dem Sie daraus echten Text machen wollen, ist eine Entscheidung, die Sie bewusst treffen müssen. Drittens gibt es zwei Alphabete, die sich nahezu identisch ansehen, das Standardalphabet und das URL-sichere, und sie zu verwechseln ist eine Lieblingsart, Daten zu verlieren, ohne überhaupt einen Fehler zu bekommen. Dieser Leitfaden führt Sie an allen drei vorbei, damit Sie beim nächsten Mal, wenn eine Mauer aus Kauderwelsch in Ihrem Terminal landet, lächeln können, statt die Augen zusammenzukneifen.
Das komplette Dekodiermenü
Öffnen Sie das base64-Modul, und Sie finden zwei Generationen von Schnittstellen nebeneinander. Die moderne ist um b64decode zentriert, wandelt bytes-ähnliche Objekte (und normale ASCII-Strings) zurück in Bytes und spricht beide Base64-Dialekte, die RFC 4648 definiert. Die Legacy-Version ist älter und dateiorientiert: Sie arbeitet mit Datei-Objekten, kennt nur das Standardalphabet und wurde um die 76 Zeichen langen umgebrochenen Zeilen herumgebaut, die RFC 2045, der MIME-Mail-Standard von 1996, von kodierter Ausgabe verlangte. Die Legacy-Namen werden Sie in reichlich Code treffen, der schon eine Weile unterwegs ist, also hier die komplette Dekodier-Seite des Menüs:
| Funktion | Was sie tut | Anmerkungen |
|---|---|---|
base64.b64decode(s, altchars=None, validate=False) |
das Arbeitstier: einen Base64-Blob zurück in rohe Bytes | akzeptiert Bytes oder einen ASCII-String, gibt immer Bytes zurück |
base64.standard_b64decode(s) |
die gleiche Arbeit, fest auf das Standardalphabet eingestellt | praktisch, wenn Sie den Dialekt mit Sicherheit kennen |
base64.urlsafe_b64decode(s) |
liest das URL-sichere Alphabet mit - und _ |
die, die JWTs liest |
base64.decodebytes(s) |
dekodiert eine oder mehrere umgebrochene Zeilen Base64 | seit Python 3.1, der MIME-freundliche Weg, nachsichtig |
base64.decode(input, output) |
streamt eine Base64-Datei in eine Rohdatei | Legacy, liest zeilenweise, nachsichtig |
base64.b32decode(s, casefold=False) |
dekodiert den kleineren Base32-Verwandten | casefold akzeptiert kleingeschriebene Eingabe |
base64.b16decode(s, casefold=False) |
dekodiert Base16, das schlicht Hexadezimal ist | in Python 3.14 bis zu sechsmal schneller |
binascii.a2b_base64(s, strict_mode=False) |
die C-Funktion, die die eigentliche Arbeit erledigt | ein direkter Hebel für Strenge, mit strict_mode seit Python 3.11 |
Alles, was unten kommt, baut auf die erste Zeile auf. Eine Tatsache ist es wert, zu wissen, bevor Sie tiefer eintauchen: In der offiziellen Dokumentation lebt das Modul unter "Internet-Datenverarbeitung", direkt neben binascii, und diese Platzierung ist kein Zufall. b64decode ist eine dünne Hülle, die das Alphabet übersetzt (wenn Sie altchars übergeben) und dann die C-Funktion binascii.a2b_base64 das schwere Heben erledigen lässt. Deshalb ist die Funktion schnell, und deshalb haben ihre Fehlermeldungen den knappen, unsentimentalen Geschmack von C.
Das Arbeitstier: b64decode
Hier ist der gesamte Vertrag, kurz genug, um ihn im Kopf zu behalten. Die Funktion nimmt ein bytes-ähnliches Objekt oder einen ASCII-String, einen optionalen Zwei-Zeichen-Alphabet-Tausch und einen Validierungsschalter an. Sie gibt ein bytes-Objekt zurück. Bei Fehlschlag wirft sie binascii.Error, das eine Unterklasse von ValueError ist, falls Sie je eine ganze Familie von Ausnahmen auf einmal fangen müssen:
import base64
data = base64.b64decode("Zm9vYmFy")
print(data)
# b'foobar'
print(type(data))
# <class 'bytes'>
Diese letzte Zeile ist die eine, wichtigste Zeile in diesem Artikel. Das Ergebnis sind Bytes, kein String, und Python hält Ihnen die Hand genau so weit, wie es sollte: Das Ausgeben des Objekts zeigt Ihnen die b'...'-Darstellung, und der Versuch, es an einen String anzukleben, wirft eine TypeError. Im Moment, in dem Sie echten Text wollen, treffen Sie die Entscheidung selbst, und die Charset-Sektion unten deckt ab, wann diese Entscheidung einfach ist und wann eine Falle.
Das optionale Argument altchars tauscht das + und / des Standardalphabets gegen ein anderes Zeichenpaar. Genau das ist der Drehregler, der den URL-sicheren Dialekt produziert, und so wird urlsafe_b64decode auf b64decode aufgebaut. Sie werden altchars selbst nur selten anfassen, aber es ist gut zu wissen, dass der Mechanismus da ist. Für alles andere erledigt die Funktion einfach die Arbeit, schnell, in C.
Standardmäßig nachsichtig, auf Wunsch streng
Standardmäßig ist b64decode ein höflicher Vergesser. Jedes Zeichen, das nicht im 64-Zeichen-Alphabet steht (und nicht in Ihren altchars), wird stillschweigend weggeschmissen, bevor das Dekodieren beginnt, und alles, was übrig bleibt, wird dekodiert. Keine Warnung, keine Anzeige, kein Rückgabewert, den man prüfen müsste, einfach nur ein Ergebnis. Diese Toleranz hat einen edlen Ahnen: Abschnitt 6.8 von RFC 2045 sagt Decodern, dass "alle Zeilenumbrüche und anderen Zeichen, die sich nicht in Tabelle 1 finden, ignoriert werden müssen", weil SMTP historisch lange Zeilen umbrochen und unterwegs verirrte Zeichen verstreut hat. Ein Payload, der durch einen Mail-Client, eine Chat-App oder eine PDF-Kopie gereist ist, lässt sich oft ganz ohne Vorbereitung dekodieren, und das ist eine echte Superkraft.
Diese gleiche Freundlichkeit ist auch der Grund, warum der Standard-Decoder als Validator nutzlos ist. Abschnitt 12 von RFC 4648 nennt das Risiko ausdrücklich: Zeichen außerhalb des Alphabets zu ignorieren, statt die gesamte Kodierung abzulehnen, öffnet einen verdeckten Kanal, der genutzt werden kann, um Informationen zu lecken, und er kann String-Gleichheitsprüfungen kaputt machen, denn zwei verschiedene Eingaben können zu denselben Bytes dekodieren. Für alles, was Sie nicht selbst kodiert haben, übergeben Sie validate=True und behandeln Sie die Ausnahme als die Antwort. Hier ist der Schadensbericht, jede Zeile auf jedem modernen Python reproduzierbar:
| Was reingeht | Nachsichtig (Standard) | validate=True |
|---|---|---|
Zm9vYmFy (ein sauberer Payload) |
b'foobar' |
b'foobar' |
Zm9v\r\nYmFy (Zeilenumbruch in der Mitte) |
b'foobar' |
binascii.Error |
Zm9v YmFy (zusätzliche Leerzeichen) |
b'foobar' |
binascii.Error |
Zm9v!YmFy (ein verirrtes Ausrufezeichen) |
b'foobar' |
binascii.Error |
junkZm9vYmFy (ein Wort vor dem Payload) |
b'\x8e\xe9\xe4foobar' |
b'\x8e\xe9\xe4foobar' |
Zm9v=YmFy (ein Padding in der Mitte) |
b'foobar' |
binascii.Error |
=Zm9v (Padding ganz vorne) |
b'foo' |
binascii.Error |
==== (vier Paddings, keine Daten) |
b'' |
binascii.Error |
(leere Eingabe) |
b'' |
b'' |
Sehen Sie der nachsichtigen Spalte bei ihrer stillen Arbeit zu. Die Zeile, die die Leute zuerst überrascht, ist die mit dem Wort ganz vorne: Alle vier Buchstaben von junk sitzen zufällig im Base64-Alphabet, also dekodiert der "Müll" als drei echte Bytes und wird mit geradem Gesicht an Ihren Payload geklebt. Der strenge Modus rettet in dieser Zeile nicht, denn die Eingabe ist wirklich gültiges Base64; es sind die anderen Zeilen, die glatt abgewiesen werden, und die Ablehnungen haben genau eine Form: ein binascii.Error mit einer von ein paar einprägsamen Meldungen:
Incorrect padding- die Länge ist nach dem Verwerfen kein Vielfaches von vier, oder eine letzte Gruppe ist zu kurz. Ein String wieZm9vYmEohne jegliches Padding landet hier.Invalid base64-encoded string: number of data characters (N) cannot be 1 more than a multiple of 4- die Eingabe ist genau ein Zeichen zu kurz für die nächste Gruppe. Das ist der klassische Fingerabdruck eines abgeschnittenen oder kopierten Payloads.Only base64 data is allowed- ein Zeichen außerhalb des Alphabets hat den strengen Modus erreicht, und ein einzelner Zeilenumbruch zählt als eines.Excess padding not allowed- Paddings in der Mitte des Strings, oder mehr Paddings, als die letzte Gruppe erlaubt.Leading padding not allowed- der String beginnt mit=.- Und eines aus einer anderen Familie:
ValueError: string argument should contain only ASCII characters, das Sie bekommen, wenn Sie einen String mit nicht-ASCII-Buchstaben übergeben. Strings werden akzeptiert, aber nur ASCII-Strings.
Im Hintergrund ist validate=True überhaupt kein eigener Code-Pfad. Das Modul leitet das Flag an binascii.a2b_base64 als seinen strict_mode-Parameter weiter, den Strenge-Check, der in Python 3.11 zu binascii hinzugefügt wurde. Das gibt Ihnen einen direkten Hebel, wenn Sie Strenge wollen, ohne den Weg über die base64-Schicht zu nehmen:
import binascii
line = b"Zm9vYmFy"
print(binascii.a2b_base64(line, strict_mode=True))
# b'foobar'
Ein Sonderfall, den Sie festnageln sollten, bevor Sie dem strengen Modus blind vertrauen: Er lehnt sogar einen einzelnen Zeilenumbruch am Ende ab, also ist ein MIME-gepackter Block eine Aufgabe für den nachsichtigen Weg oder für decodebytes, nicht für validate=True. Halten Sie den strengen Weg für Daten, die Sie perfekt sauber erwarten, wie ein frisch geprägtes Token direkt aus Ihrem eigenen Code.
base64url, das Alphabet, das in URLs passt
Das Standardalphabet hat zwei Zeichen, die URLs hassen. Das + Zeichen wird von jedem Formular-Decoder als Leerzeichen gelesen, und das / Zeichen ist für Pfadtrenner reserviert. Abschnitt 5 von RFC 4648 definiert den verwandten Dialekt, in dem + zu - und / zu _ wird und das Padding fallengelassen wird, wann immer die Datenlänge aus dem Kontext bekannt ist. Der RFC gibt der Variante sogar einen offiziellen Namen, base64url, und besteht darauf, dass sie nicht einfach "base64" heißen soll. Am häufigsten treffen Sie sie in JSON Web Tokens, wo jeder Teil des Tokens base64url ohne Padding ist, und sie taucht auch in OAuth-Tokens und API-Cursor-Parametern auf.
Python liefert eine dedizierte Funktion dafür, urlsafe_b64decode. Sie übersetzt die Gedankenstriche und Unterstriche zurück in Plus und Slash und dekodiert dann, aber sie wird nicht für Sie nachpaddingen. Ungepaddingte Eingabe ist der Normalfall bei JWTs, also kommt die Rechenzeile zuerst, und es ist dieselbe, die Bibliotheken wie PyJWT unter der Haube verwenden:
import base64
segment = "Zm9vYmE"
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'
Der Ausdruck "=" * (-len(segment) % 4) sieht aus wie ein Trick, aber er ist die ganze Arbeit: Er erzeugt null, eins oder zwei Paddings und nie drei, damit ein bereits gepaddeter String unverändert durchgeht. Das negative Modulo ist es, was ihn für Strings jeder Länge funktionieren lässt, und es ist die eine Zeile Base64-Mathematik, die jeder Python-Entwickler mindestens einmal tippt.
Jetzt die gefährliche Verwechslung, denn die beiden Alphabete sehen sich so ähnlich, dass man sie verwechseln kann. Führen Sie einen base64url-String durch den Standard-Decoder, und die Gedankenstriche und Unterstriche sind einfach nicht im Standardalphabet, also verschluckt der nachsichtige Decoder sie und dekodiert, was übrig bleibt. Für manche Payloads ist das ein vermurkster Byte-Stream; für andere gar nichts:
import base64
tricky = base64.urlsafe_b64encode(b"\xfb\xff\xfe")
print(tricky)
# b'-__-'
print(base64.standard_b64decode(tricky))
# b'' - jedes Zeichen wurde stillschweigend verworfen
print(base64.urlsafe_b64decode(tricky))
# b'\xfb\xff\xfe'
Die umgekehrte Richtung ist nachsichtig, und genau das macht die Verwechslung unbemerkt: urlsafe_b64decode übersetzt erst sein Alphabet und dekodiert dann nachsichtig, also akzeptiert es gerne auch einen Standard-Alphabet-String mit + und / drin. Die Lektion lautet: Nicht improvisieren. Sie ist, eine Funktion pro Dialekt zu wählen und dabei zu bleiben, so wie bei einer Fremdwährung: Den Yen dort ausgeben, wo der Yen gültig ist, nicht im falschen Wechselbüro.
Die Ausgabe sind Bytes: Das Charset-Gespräch
Hier ist der Satz, der die Hälfte der Charset-Fragen beantwortet, die Leute bei Base64 aufwerfen: b64decode dekodiert Bytes; es dekodiert keinen Text. Es gibt kein Charset-Argument, es gibt keine Umwandlung, und nichts an der Eingabe sagt Python, was die Bytes bedeuten sollen. Die Bedeutung ist etwas, das Sie aus dem Kontext beisteuern müssen, und dieser Kontext ist fast immer eines von drei Dingen: ein Header, der es sagt, ein API-Vertrag, der es sagt, oder eine magische Zahl, die sich in den Bytes selbst versteckt.
import base64
raw = base64.b64decode("w6l0w6k=")
print(raw)
# b'\xc3\xa9t\xc3\xa9'
print(raw.decode("utf-8"))
# été
Dasselbe mit dem falschen Etikett ist ein lautes Scheitern, was eine Gnade ist. Bytes, die kein gültiges UTF-8 sind, weigern sich, ein String zu werden, und die Ausnahme sagt Ihnen genau, welches Byte beleidigt war:
import base64
raw = base64.b64decode("/w==")
try:
raw.decode("utf-8")
except UnicodeDecodeError as caught:
print(caught)
# 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte
Drei Faustregeln halten diese Sektion davon ab, eine Horror-Show zu werden. Eins: Wenn die dekodierten Daten JSON sind, müssen Sie gar nicht manuell dekodieren, denn json.loads akzeptiert seit Python 3.6 direkt Bytes und erkennt UTF-8, UTF-16 und UTF-32 auf eigene Faust. Zwei: Binär ist kein Text, also ist ein "erkanntes" Charset für ein PNG ein glücklicher Wurf und keine Tatsache; prüfen Sie die Bytes statt des Etiketts. Drei: Wenn der Absender Ihnen das Charset gesagt hat, glauben Sie dem Absender, denn ein Content-Type-Header oder ein API-Dokument schlägt jedes Erkennungswerkzeug, jedes einzelne Mal.
Wo dekodiertes Base64 in Python-Code auftaucht
Nach einer Weile erkennen Sie die Formen. Hier ist der Feldführer für die Orte, an denen dekodiertes Base64 in einer Python-Anwendung auftaucht, und das Einzeilen-Rezept für jedes. Die folgenden Sektionen widmen den häufigsten die volle Behandlung:
| Wo Sie es finden | Was es ist | Wie Sie es lesen |
|---|---|---|
| Ein JWT | Header-, Payload- und Signaturteile (RFC 7519) | an den Punkten aufteilen, urlsafe_b64decode mit dem Padding-Fix |
Ein Authorization-Header |
HTTP-Basic-Anmeldedaten, user:pass (RFC 7617) |
das Basic-Präfix abschneiden, dekodieren, am ersten Doppelpunkt aufteilen |
Eine data:-URI |
inlinedes Medium in HTML oder CSS (RFC 2397) | am ersten Komma abschneiden, den Rest dekodieren |
| Ein Mail-Anhang | ein Content-Transfer-Encoding: base64-Körper (RFC 2045) |
get_payload(decode=True) auf dem Nachrichtenteil |
| Ein Mail-Header-Wert | ein =?charset?b?...?= kodiertes Wort (RFC 2047) |
lassen Sie das email-Paket es für Sie dekodieren |
| Eine PEM-Datei | ein gepanzerter Schlüssel oder Zertifikat (RFC 7468) | die Panzerzeilen entfernen, den Körper zu DER dekodieren |
| Ein JSON-API-Feld | Binär, das als String durchgeschmuggelt wird | dekodieren, dann das Ergebnis als Bytes behandeln, nicht als Text |
| Eine TEXT-Spalte oder eine Umgebungsvariable | Binär oder JSON, das an einem rein textbasierten Ort gespeichert ist | dekodieren, dann parsen oder schreiben, mit dem vereinbarten Charset |
Ein JSON Web Token lesen
Ein JWT ist drei base64url-Stücke, die durch Punkte verbunden sind: ein Header, ein Payload und eine Signatur. Die ersten beiden sind reines JSON, also ist ein Reinschauen jeweils eine Zeile, mit dem Padding-Fix aus der Sektion oben:
import base64
import json
def read_part(segment):
padded = segment + "=" * (-len(segment) % 4)
return base64.urlsafe_b64decode(padded)
token = ("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0."
"8Rmup2hf8jZvoBgoCRqRWlBFNtvUYmA0eR7YKellPMs")
head, body, _signature = token.split(".")
print(json.loads(read_part(head)))
# {'alg': 'HS256', 'typ': 'JWT'}
print(json.loads(read_part(body)))
# {'sub': '1234567890', 'name': 'John Doe'}
Ein Abgrenzungshinweis, denn er ist wichtig: Ein Token auf diese Weise zu inspizieren ist ein Debugging-Werkzeug, kein Authentifizierungsmechanismus. Dass der Payload lesbar ist, bedeutet nicht, dass er echt ist; ein Angreifer kann die ersten beiden Segmente fälschen, ohne je Ihr Geheimnis zu kennen. Für die echte Verifikation geben Sie das Token an PyJWT (pip install pyjwt), das die Signatur prüft und ohne eine explizite Algorithmenliste weigert, zu dekodieren:
import jwt
# Ein Schlüssel unter 32 Bytes verdient PyJWTs InsecureKeyLengthWarning (PyJWT 2.11+), ein faires Murren für einen Demo-Schlüssel.
decoded = jwt.decode(token, "super-secret-key", algorithms=["HS256"])
print(decoded)
# {'sub': '1234567890', 'name': 'John Doe'}
Mit einem falschen Schlüssel bekommen Sie eine Ausnahme statt eines Wörterbuchs, genau das Verhalten, das Sie in Produktionscode wollen. Und wenn das Token mit einem abgelaufenen Zeitstempel ankam, wirft PyJWT auch dafür eine Ausnahme, damit Sie sich nie selbst die Namen der Claims merken müssen.
Eine Data URI öffnen
Data-URIs betten Medien direkt in HTML oder CSS ein, damit der Browser keinen zweiten Request losfeuert: data:, der Medientyp, das Wort base64, ein Komma und die kodierten Bytes. Die Trennung liegt am ersten Komma, Schluss, und alles danach ist ein gewöhnlicher Standard-Alphabet-Payload:
import base64
uri = ("data:image/png;base64,"
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJ"
"AAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==")
mime, payload = uri.split(",", 1)
data = base64.b64decode(payload)
print(mime)
# data:image/png;base64
print(data[:8])
# b'\x89PNG\r\n\x1a\n'
Die PNG-Signatur aus den ersten acht Bytes am Anfang des Ergebnisses ist ein günstiger und fröhlicher Check, dass Sie die richtige Sache dekodiert haben. Zwei Stolperfallen verdienen eine Erwähnung. Wenn die URI von einer gecrapten Seite oder einer Chat-Nachricht stammt, entfernen Sie erst HTML-Entitäten und verirrten Leerraum, denn der nachsichtige Decoder verzeiht viel Müll und reicht Ihnen ein kaputtes Bild statt eines Fehlers. Und wenn Sie unzuverlässige Eingabe im großen Stil dekodieren, übergeben Sie validate=True: Eine Data-URI, die die strenge Validierung nicht besteht, war nie wohlgeformt, und Sie wollen sie nicht auf Verdacht auf die Platte schreiben.
Den Authorization-Header knacken
Die Basic-Authentifizierung (RFC 7617) ist das älteste Verfahren in HTTP, und sie hält immer noch eine überraschende Anzahl von API-Integrationen, Webhooks und CI-Pipelines am Leben. Der Client schickt seine Anmeldedaten als user:pass, base64-kodiert, hinter dem Wort Basic:
import base64
header = "Basic amFuZTpwYTpzcw=="
decoded = base64.b64decode(header[len("Basic "):]).decode("utf-8")
user, _, password = decoded.partition(":")
print(user, password)
# jane pa:ss
Achten Sie auf das partition, denn es ist das Detail, das Sie später rettet: Das Passwort kann Doppelpunkte enthalten, die Benutzer-ID nicht, und nur der erste Doppelpunkt ist der Trenner. Ein ehrlicher Hinweis, denn der RFC selbst ist dabei direkt: base64 ist keine Verschlüsselung. RFC 4648 sagt, dass die Base-Kodierung "sonst leicht erkennbare Informationen, wie Passwörter, visuell versteckt, aber keine rechnerische Vertraulichkeit bietet". Ein Basic-Header kann von jedem dekodiert werden, der den Traffic sieht, also behandeln Sie ihn als Bequemlichkeit für TLS-geschützte Verbindungen, nicht als Sicherheitsgrenze. Wenn Sie derjenige sind, der den Header schickt, baut requests ihn für Sie mit auth=("jane", "pa:ss"), was sich lohnt, wann immer die Bibliothek schon in Ihrem Stack ist.
E-Mail, der ursprüngliche Kunde
Base64 wurde 1993 genau für einen Job standardisiert: Binärdaten über E-Mail überleben zu lassen. RFC 2045, der MIME-Standard, definierte die Content-Transfer-Encoding: base64 Körper-Kodierung, und sie ist immer noch der Standardweg, wie Anhänge über das Internet reisen. Pythons email-Paket erledigt die ganze Arbeit für Sie: Es parst die Header, dekodiert die =?utf-8?b?...?= kodierten Wörter, die RFC 2047 in Headerfeldern versteckt, und base64-dekodiert Körper, wenn Sie darum bitten:
import email
from email import policy
raw = (b"Subject: =?utf-8?b?w6l0w6k=?=\r\n"
b"From: sender@example.com\r\n"
b"To: reader@example.com\r\n"
b"Content-Transfer-Encoding: base64\r\n"
b"\r\n"
b"w6l0w6kgbWFpbA==\r\n")
msg = email.message_from_bytes(raw, policy=policy.default)
print(msg["Subject"])
# été
print(msg.get_payload(decode=True))
# b'\xc3\xa9t\xc3\xa9 mail'
Der get_payload(decode=True)-Aufruf liest den Content-Transfer-Encoding-Header und base64-dekodiert den Körper für Sie und wickelt dabei die Zeilen mit 76 Zeichen auf. Das Argument policy=policy.default wählt die moderne Schnittstelle seit Python 3.6, als die neue policy-basierte E-Mail-API aufhörte, vorläufig zu sein, und liefert Ihnen dekodierte Header-Werte ab Werk; der Legacy-Parser funktioniert immer noch, aber Sie kommen nicht drumherum, die kodierten Wörter von Hand zu dekodieren. Sie gehen nur zu decodebytes hinab, wenn Sie einen nackten Ausschnitt parsen, der keine komplette Nachricht ist, wie ein Block, den jemand in ein Ticket kopiert hat. Für Multipart-Nachrichten iterieren Sie mit iter_attachments() und geben Sie jedem Teil dieselbe Einzeilen-Behandlung.
PEM-Panzerung und das cryptography-Paket
Eine PEM-Datei ist eine Header-Zeile, etwas umgebrochenes Base64 und eine Footer-Zeile, und nichts weiter. Die Panzerung ist dekorativ; das Base64 ist die ganze Geschichte, denn es dekodiert in die rohe DER-Struktur darunter. Das cryptography-Paket (pip install cryptography) kann das Ergebnis direkt laden, und deshalb ist es das Standardwerkzeug für alles, was Zertifikate und Schlüssel betrifft:
import base64
from cryptography import x509
pem = b"""-----BEGIN CERTIFICATE-----
MIIBGzCBwaADAgECAgEBMAoGCCqGSM49BAMCMBcxFTATBgNVBAMMDGV4YW1wbGUu
dGVzdDAeFw0yNjA4MjkxNzIxMzZaFw0yNjA4MzAxNzIxMzZaMBcxFTATBgNVBAMM
DGV4YW1wbGUudGVzdDBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABPvNHjdF4b1n
SkBDT6UWtG2k8ICe45eL3kSkVfuhriev1uO9PBLMP50HWnrLbCXtl3lhWaVibctl
QbWRG4xqGLcwCgYIKoZIzj0EAwIDSQAwRgIhAKdFm5GLecg2fF7qUhSmKGtgNFaL
qVyKtDXK07N6GZd/AiEAtRXemnYqDMz77o9+VpM/NsNEwDi0yaVB+tKGLbdKJb0=
-----END CERTIFICATE-----
"""
body = b"".join(pem.splitlines()[1:-1])
der = base64.b64decode(body)
cert = x509.load_der_x509_certificate(der)
print(cert.subject.rfc4514_string())
# CN=example.test
In fast jedem Produktionscode tun Sie die Panzerung und das Dekodieren nie von Hand: load_pem_x509_certificate akzeptiert die gepanzerten Bytes und erledigt den Base64-Schritt für Sie unter der Haube. Der manuelle Weg verdient sich seinen Lohn, wenn die DER-Bytes schon in Ihren Händen sind (eine Datenbankspalte, eine Konfigurationsdatei, ein Puffer aus einem Protokoll), oder wenn der Block in einem String verpackt ankam und Sie sehen wollen, was drin ist, bevor Sie ihm vertrauen. Schlüssel funktionieren auf dieselbe Weise, mit load_der_private_key, der auf der anderen Seite desselben Dekodierens wartet.
Dateien, Magische Zahlen und die .b64-Gewohnheit
Dekodieren ist nur die Hälfte der Arbeit; die Bytes wollen in der Regel eine Datei. Das Muster ist lesen, dekodieren, prüfen, schreiben, und die Prüfung zählt, denn ein kaputter Payload würde sonst eine stillschweigend falsche Datei erzeugen, die Sie erst Wochen später entdecken:
import base64
import binascii
with open("payload.b64", "rb") as handle:
encoded = handle.read()
try:
data = base64.b64decode(encoded, validate=True)
except binascii.Error:
data = base64.b64decode(encoded)
with open("payload.bin", "wb") as out:
out.write(data)
Für schnelle Einmal-Konvertierungen erledigt die Legacy-Datei-zu-Datei-Funktion die ganze Reise in einem einzigen Aufruf, umgebrochene Zeilen inklusive:
import base64
with open("photo.b64", "rb") as src, open("photo.png", "wb") as dst:
base64.decode(src, dst)
Was haben Sie gerade überhaupt dekodiert? Die ersten Bytes fast jedes gängigen Formats sind eine feste Signatur, und weil Base64 deterministisch ist, ist die kodierte Signatur es auch. Einen dieser Präfixe zu sehen ist wie ein Kennzeichen in der Ferne zu erkennen:
| Der Base64 beginnt mit | Es ist wahrscheinlich |
|---|---|
iVBORw0KGgo |
ein PNG-Bild |
/9j/ |
ein JPEG-Bild |
R0lGODlh |
ein GIF-Bild |
JVBERi0 |
ein PDF-Dokument |
UEsDBA== |
ein ZIP-Archiv |
UklGRg== |
ein RIFF-Container (WAV, WEBP, AVI) |
LS0tLS1CRUdJTg== |
ein ASCII-gepanzerter Block ("-----BEGIN ...") |
Und machen Sie die Größen-Mathematik gleich mit, während Sie die Datei schreiben, denn das ist die Zahl, die die Leute überrascht, wenn die Platte voll wird: Kodieren bläht Daten um etwa ein Drittel auf, also reist eine 300-KB-Datei als etwa 400 KB Base64-Text, und die Datei, die Sie zurückdekodieren, hat wieder die kleinere, ursprüngliche Größe. Ihre Platte, und Ihr Speicher, wenn Sie die ganze Datei auf einmal lesen, sollten für den Unterschied einkalkulieren.
Datenbanken, Konfigurationsdateien und Umgebungsvariablen
Base64 ist ein Liebling, um Binär (oder JSON) durch Speicher zu schmuggeln, der nur Text annimmt: eine TEXT-Spalte, ein Wert in einer .ini-Datei, eine Umgebungsvariable in einer Deploy-Pipeline. Das Dekodier-Rezept ist dasselbe wie für Dateien, nur ohne die Platte:
import base64
import json
stored = "eyJyb2xlIjogImFkbWluIiwicHJvamVjdCI6Im15c2l0ZSJ9"
payload = json.loads(base64.b64decode(stored))
print(payload)
# {'role': 'admin', 'project': 'mysite'}
Zwei Anmerkungen für diese Ecke des Hauses. Wenn der gespeicherte Wert JSON ist, überspringen Sie den Zwischenstopp .decode("utf-8") und lassen Sie json.loads die Bytes direkt annehmen, denn es tut das seit Python 3.6. Und eine ehrliche Warnung, denn hier lebt das teuerste Missverständnis im ganzen Artikel: Base64 in einer Umgebungsvariable oder einer Konfigurationsdatei ist ein Schild gegen den Menschen, der nur einen Blick in die Datei wirft, nicht gegen den, der sie liest. Wenn der Wert wirklich sensibel ist, verschlüsseln Sie ihn erst (das cryptography-Paket liefert Fernet genau dafür) und erst dann base64-kodieren Sie den Geheimtext, wenn Ihr Speicher Text verlangt.
Wenn der Payload in Stücken ankommt
Die Standardbibliothek hat keinen inkrementellen Base64-Decoder: Es gibt kein update-und-finish-Paar, also brauchen Streaming-Daten etwas Buchführung von Ihnen selbst. Die Mathematik ist einfach und streng zugleich. Vier kodierte Zeichen ergeben drei Bytes, also können Sie nur vollständige Vier-Zeichen-Gruppen dekodieren, und Sie müssen den Rest in den nächsten Chunk mitnehmen:
import base64
def chunked_decode(chunks):
out = []
leftover = b""
for chunk in chunks:
buffer = leftover + chunk
whole = len(buffer) // 4 * 4
if whole:
out.append(base64.b64decode(buffer[:whole]))
leftover = buffer[whole:]
if leftover:
out.append(base64.b64decode(leftover + b"=" * (-len(leftover) % 4)))
return b"".join(out)
Füttern Sie ihr einen Socket-Puffer, eine Datei, die in 64-KB-Stücken gelesen wird, oder einen Generator von Zeilen mit gestrichenen Zeilenumbrüchen, und die Ausgabe ist identisch mit dem Dekodieren der ganzen Sache in einem Rutsch. Wenn Ihre Eingabe garantiert sauber und nicht umgebrochen ist, behalten Sie die Strenge bei, indem Sie jede vollständige Gruppe mit validate=True dekodieren, und denken Sie daran, dass der letzte Rest den Padding-Fix brauchen kann, deshalb fügt der Helper ihn vor dem letzten Dekodieren hinzu. Das ist dieselbe Naht-Logik, die die Encoder auf der anderen Seite verwenden, nur mit vier Zeichen statt drei Bytes.
Von der Kommandozeile
Das base64-Modul dient gleichzeitig als kleines Kommandozeilen-Tool, was praktisch ist, wenn der Payload in Ihrem Terminal sitzt statt in Ihrem Code. Kodieren ist der Standard; -d (oder sein Zwilling, -u) dekodiert:
echo -n "hello world" | python3 -m base64
aGVsbG8gd29ybGQ=
echo -n "aGVsbG8gd29ybGQ=" | python3 -m base64 -d
hello world
Es liest von stdin, wenn Sie ihm keine Datei geben, oder von der Datei, die Sie nennen, und unter der Haube ist es die Legacy-Datei-zu-Datei-Schnittstelle, also kommt die Ausgabe umgebrochen bei 76 Zeichen mit einem Zeilenumbruch am Ende jeder Zeile. Für das Einfügen eines Payloads in eine Sitzung mit aufgedrehter Strenge ist die Einzeiler-Version des Decoders eine gute Gewohnheit:
import base64
import sys
print(base64.b64decode(sys.stdin.read(), validate=True))
Neun Wege, sich zu verbrennen
Jeder Base64-Dekodierungs-Bug in Python ist einer dieser. Halten Sie die Liste irgendwo bereit, wo Sie sie in der Panik finden, denn sie hat mehr Nachmittage eingefangen als jedes andere einzelne Dokument, das Sie dieses Jahr lesen werden. Die ersten drei kommen mit Code, denn sie sind leichter zu merken, sobald man das Wrack gesehen hat:
Das fehlende Padding. Der häufigste Crash von allen, normalerweise, weil ein JWT-Teil oder ein API-Wert ohne seine Paddings ankam:
import base64
import binascii
segment = "Zm9vYmE"
try:
base64.urlsafe_b64decode(segment)
except binascii.Error as caught:
print(caught)
# Incorrect padding
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'
Der abgeschnittene String. Wenn der Fehler sagt, die Anzahl der Datenzeichen "cannot be 1 more than a multiple of 4" (kann nicht 1 mehr als ein Vielfaches von 4 sein), wurde der Payload unterwegs abgeschnitten, oder ein Kopieren-Einfügen ließ am Ende ein Zeichen fallen. Keine Menge an Padding repariert einen String, dessen Länge modulo vier eins ist; die Daten sind einfach nicht da, und die ehrliche Antwort ist, den Payload noch einmal zu verlangen.
Der stille Müll. Der nachsichtige Modus dekodiert, was übrig bleibt, und gewöhnliche englische Wörter sind voll von Base64-Alphabet-Buchstaben, also wird ein verirrtes Wort vor dem Payload zu echten Bytes, die an Ihre Daten geklebt sind:
import base64
print(base64.b64decode("junkZm9vYmFy"))
# b'\x8e\xe9\xe4foobar' - drei Bytes pure Fiktion, dann die Wahrheit
Die anderen sechs brauchen überhaupt keinen Code:
- Sie haben einen base64url-String mit dem Standard-Decoder dekodiert. Die Gedankenstriche und Unterstriche sind nicht im Standardalphabet, also verschwanden sie stillschweigend und der Payload kam vermurkst oder leer heraus. Verwenden Sie
urlsafe_b64decodemit dem Padding-Fix. - Sie haben vergessen, dass das Ergebnis Bytes sind. Das Ankleben an einen String wirft eine
TypeError, und das Einschieben in eine JSON-Antwort serialisiert dieb'...'-Darstellung. Rufen Sie.decode(encoding)an der Grenze auf, bewusst, mit der Kodierung, die Sie wirklich meinen. - Sie haben einen nicht-ASCII-String übergeben. Der Decoder akzeptiert Strings, aber nur ASCII-Strings; alles andere ist eine
ValueError. Wenn Ihr Payload aus einer Textdatei kam, die mit der falschen Kodierung gelesen wurde, reparieren Sie das Lesen, nicht das Dekodieren. - Sie haben zweimal dekodiert. Die Daten wurden upstream schon dekodiert, oder es war Base64 von Base64, und der zweite Durchgang machte aus Ihrem Passwort sechs Bytes, die kein Mensch je wieder lesen wird.
- Sie haben den strengen Modus auf umgebrochene Daten angewendet. Ein einzelner Zeilenumbruch reicht, um
validate=Truewerfen zu lassen, also gehören MIME-Blöcke und PEM-Körper zu den nachsichtigen Werkzeugen, nicht zum strengen. - Sie haben ein Padding in der Mitte vertraut. Im nachsichtigen Modus wird ein
=irgendwo im String stillschweigend verworfen, also kann ein kaputter Payload mit einem fehlplatzierten Padding zu der "richtigen" Antwort dekodieren. Nur der strenge Modus merkt es, und er merkt es, indem er ablehnt.
Wenn Ihre Aufgabe darin besteht, der Türsteher zu sein, ist hier ein kleiner Helper, der die beiden Launen zusammenarbeiten lässt: erst streng, dann Padding-Fix, und ein lautes Scheitern, wenn beides nicht hilft:
import base64
import binascii
def safe_decode(text):
candidate = text.strip()
try:
return base64.b64decode(candidate, validate=True)
except binascii.Error:
padded = candidate + "=" * (-len(candidate) % 4)
return base64.b64decode(padded, validate=True)
print(safe_decode("Zm9vYmE"))
# b'fooba'
print(safe_decode("Zm9vYmFy"))
# b'foobar'
Beachten Sie, dass der Helper nur das Alphabet vertraut, das Sie ihm mitteilen. Wenn Ihre Eingabe base64url sein könnte, füttern Sie sie stattdessen an urlsafe_b64decode. Validierung ist ein Vertrag, und der Vertrag sagt, in welchem Dialekt die Daten sind.
Drei Jahrzehnte eines stillen Moduls
Das Modul ist seit einem Vierteljahrhundert in der Standardbibliothek, und den größten Teil der Zeit saß es still. Wenn es sich doch bewegte, waren die Bewegungen klein, aber echt, und sie erklären einige "works on my machine"-Geschichten, die durch alte Foren schwimmen:
- 1995 - Jack Jansen schrieb
base64.pyum, um die eigentliche Arbeit an das C-Level-Modulbinasciizu delegieren. Der Kommentar ist noch immer in der Datei, und die Delegation ist noch heute wahr. - 2003, ausgeliefert in Python 2.4 - Barry Warsaw fügte die vollständige RFC 3548 Unterstützung hinzu: die
b16,b32undb64-Familien, plus diestandard_*undurlsafe_*-Varianten, die Sie heute verwenden. - Python 3.1 -
encodestringunddecodestringwurden zugunsten vonencodebytesunddecodebytesals veraltet markiert, den Namen, die hängengeblieben sind. - Python 3.3 - die Decode-Funktionen begannen, ASCII-Strings zu akzeptieren, und beendeten die Ära, in der jedes Dekodieren mit einem Bytes-Literal begann.
- Python 3.4 - jedes bytes-ähnliche Objekt (inklusive Memoryviews) wird überall akzeptiert, und die Base85-Verwandten,
a85undb85, traten zum Modul bei. - Python 3.9 - die seit Langem als veraltet markierten
encodestringunddecodestringwurden endlich entfernt. Alte Tutorials, die sie aufrufen, brauchen eine Ein-Wort-Umbenennung. - Python 3.10 -
b32hexencodeundb32hexdecodekamen mit dem erweiterten Hex-Alphabet, dem, das kodierte Daten lexikographisch sortierbar hält. - Python 3.11 -
binascii.a2b_base64bekamstrict_mode, auf demvalidate=Trueunter der Haube reitet. - Python 3.13 -
z85encodeundz85decodebrachten ZeroMQs Z85-Dialekt in die Standardbibliothek, und das uralteuu-Modul wurde unter PEP 594 mit einem deutlichen Hinweis aufbase64als Ersatz entfernt. - Python 3.14 -
b16decodewurde bis zu sechsmal schneller: Seine Validierung läuft jetzt aufbytes.translatestatt auf einem regulären Ausdruck, und das Modul importiertreüberhaupt nicht mehr. Auch seine Importzeit landete auf der Liste der verbesserten Module.
Nichts davon ändert, was die Funktionen tun, und das ist der stille Luxus eines so alten Moduls: Code, der 2005 Base64 dekodiert hat, dekodiert es 2026 immer noch, auf derselben Zeile, mit demselben Ergebnis.
Freuden aus den Rändern
Die ernste Arbeit ist erledigt, also hier die kleinen Freuden, die das Modul in seinen Rändern versteckt:
- Die eigene Dokumentation des Moduls läuft seit über einem Jahrzehnt dieselbe Demonstration:
b'data to be encoded'geht rein,b'ZGF0YSB0byBiZSBlbmNvZGVk'kommt raus. Wenn Sie die base64-Seite irgendwelcher Python-Version in den letzten zwanzig Jahren gelesen haben, haben Sie dieses Paar schon mal getroffen. - Das Wort
junkist ein völlig gültiger Base64-String. Alle vier Buchstaben sind im Alphabet, deshalb wird ein verirrtes Wort am Anfang eines Payloads zu drei Bytes Fiktion statt zu einem Fehler, und deshalb verdient sich der nachsichtige Modus seinen Spitznamen. urlsafe_b64decodeist zufällig zweisprachig. Es übersetzt erst sein Alphabet und dekodiert dann nachsichtig, also liest es auch einen Standard-Alphabet-String mit+und/drin. Eine Funktion, zwei Dialekte, null Beschwerden.- Die Fehlermeldungen sind ein stabiles Mini-Lexikon, das sich seit der C-Implementierung nicht bewegt hat:
Incorrect padding,Only base64 data is allowed,Excess padding not allowed,Leading padding not allowed. Lernen Sie sie, und Sie können einen kaputten Payload triagieren, ohne eine einzige Zeile Code auszuführen. - Der leere String ist die einzige Eingabe, die überhaupt keine Reaktion hervorruft:
b''rein,b''raus, in beiden Launen. Nichts rein, nichts raus, keine Alarmglocke. - Die Docstring des Moduls nennt immer noch RFC 3548, die 2003er Ausgabe der Spezifikation. RFC 4648 ist seit 2006 der aktuelle Standard, und das Modul folgt ihm treu, ohne sich die Mühe zu machen, den Satz zu aktualisieren.
- Python 2 hatte auf der Decode-Seite keine Typwand: ein normaler
strrein, ein normalerstrraus. Die Bytes-Grundüberholung von 2007 in der Python 3 Entwicklung änderte das, und die alten Python 2 Tutorials sind der Ort, auf den die meisten "warum ist mein Dekodieren kaputt"-Threads noch immer zeigen.
Und hier ist die ganze Philosophie in vier Regeln. Übergeben Sie validate=True für alles, was Sie nicht selbst kodiert haben, und behandeln Sie die Ausnahme als echte Antwort, nicht als Vorschlag. Wissen Sie, welchen Dialekt Sie in der Hand haben, standard, base64url oder MIME-umgebrochen, denn der Decoder wird es Ihnen nicht sagen; er wird nur raten, indem er alles wegwirft, was nicht passt. Behandeln Sie das Ergebnis als Bytes, bis Sie bewiesen haben, dass es Text ist, und fragen Sie dann, wem das Charset gehörte. Und denken Sie daran, dass die freundlichste Eigenschaft dieser Funktion, die Willigkeit, Dinge zu dekodieren, die nicht ganz Base64 sind, dieselbe Eigenschaft ist, die sie gefährlich macht, also entscheiden Sie bei jedem Aufruf, wie viel Vertrauen die Eingabe verdient.
Wenn Sie irgendwann den anderen Weg gehen müssen, um frische Bytes zurück in dieses freundliche Band aus Buchstaben zu wickeln - für ein Token, einen Anhang oder ein eingebettetes Bild - ist die ganze Geschichte von b64encode ausführlich im verwandten Base64-Kodierungs-Artikel unten auf dieser Seite abgedeckt. Die beiden Richtungen sind Spiegelbilder, aber jede hat ihre eigene Reihe an Überraschungen, und diese hier kennen Sie jetzt auswendig. Frohes Dekodieren.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in Python: Ein vollständiger Leitfaden