Base64-decodering in Python: een complete gids
Op een of andere plek in je code is zojuist een tekenreeks geland die er helemaal niet als tekst uitziet: een lange reeks van A tot Z, een paar cijfers, af en toe een + of /, misschien een - of _, en mogelijk één of twee =-tekens die aan het einde parkeren. Achter die tekenreeks kan de lading van een JWT schuilgaan die je gateway afwees, een afbeelding die verstopt zit in een pagina vol HTML, een bestand dat iemand je per e-mail stuurde als .b64-bijlage, of een certificaatblok in een ticket dat al door drie helpdesks is gereisd. Jouw taak is de originele bytes terug te leveren, precies zoals ze waren. En Python staat in prima zin voor deze klus, want de hele gereedschapskist zit al decennia in de standaardbibliotheek: één regel import base64 en je bent op elk platform klaar, zonder dat je iets moet installeren of configureren.
Een snelle herhaling terwijl je aanschuift, want dat heeft iedereen wel een keer per jaar nodig: Base64 schrijft elke groep van drie bytes om naar vier tekens uit een alfabet van 64 tekens, en als de laatste groep van drie bytes niet volledig is, vult de =-opvulling de groep aan zodat de uitvoer altijd in vieren komt. Dat is de hele truc. Het is geen compressie en geen geheimhouding, alleen een manier om binair over te laten leven via kanalen die niets accepteren behalve tekst. De startpagina van deze site behandelt het formaat in alle diepte, het alfabet en de opvulwiskunde inbegrepen, dus besteden we onze energie waar de pijn echt zit: aan de Python-zijde van decoderen, en aan het eerlijk houden van het resultaat.
Drie feiten bepalen alles dat hierop volgt, en het loont de moeite om ze te onthouden vóórdat je nog één regel verder leest. Ten eerste heeft de decoder twee stemmingen: een beleefde, vergevingsgezinde standaardmodus die alles wat hij niet herkent stilletjes weggooit, en een streng modus dat dergelijke invoer zonder meer weigert. Ten tweede is het resultaat van een decode-actie altijd een bytes-object, nooit een tekenreeks, en het moment waarop je er échte tekst van wilt is een beslissing die je bewust moet nemen. Ten derde bestaan er twee alfabetten die vrijwel identiek eruitzien, het standaardalfabet en het URL-veilige alfabet, en ze door elkaar halen is een van de favoriete manieren om data te verliezen zonder ook maar één foutmelding. Deze gids loodst je over alle drie heen, zodat je de volgende keer dat een muur onzin in je terminal landt, kunt glimlachen in plaats van knijpen.
De volledige decode-menukaart
Open de base64-module en je vindt twee generaties interface die naast elkaar zitten. De moderne, met b64decode als middelpunt, zet bytes-achtige objecten (en gewone ASCII-tekenreeksen) terug om naar bytes, en hij spreekt beide Base64-dialecten die in RFC 4648 zijn gedefinieerd. De ouderwetse is ouder en gericht op bestanden: hij werkt met bestandobjecten, kent alleen het standaardalfabet, en is opgebouwd rond de omwikkelde regels van 76 tekens die RFC 2045, de MIME-e-mailstandaard uit 1996, van gecodeerde uitvoer eiste. Je komt de ouderwetse namen in ruim voldoende code tegen die al een tijdje meeloopt, dus hier is de complete decoderingskant van het menu:
| Functie | Wat Het Doet | Opmerkingen |
|---|---|---|
base64.b64decode(s, altchars=None, validate=False) |
het werkpaard: een Base64-klomp terug naar ruwe bytes | accepteert bytes of een ASCII-tekenreeks, levert altijd bytes op |
base64.standard_b64decode(s) |
hetzelfde werk, vastgezet op het standaardalfabet | handig als je zeker weet welk dialect het is |
base64.urlsafe_b64decode(s) |
leest het URL-veilige alfabet met - en _ |
de lezer van JWT's |
base64.decodebytes(s) |
decodeert één of meer omwikkelde Base64-regels | toegevoegd in Python 3.1, de MIME-vriendelijke route, soepel |
base64.decode(input, output) |
streamt een Base64-bestand naar een ruw bestand | ouderwets, leest regel voor regel, soepel |
base64.b32decode(s, casefold=False) |
decodeert de kleinere Base32-neef | casefold accepteert invoer in kleine letters |
base64.b16decode(s, casefold=False) |
decodeert Base16, dat gewoon hexadecimaal is | tot zes keer sneller in Python 3.14 |
binascii.a2b_base64(s, strict_mode=False) |
de functie op C-niveau die het echte werk doet | een directe greep naar de strengheid, met strict_mode sinds Python 3.11 |
Alles hieronder bouwt voort op de eerste regel. Eén feit is de moeite waard om te weten vóórdat je dieper duikt: in de officiële documentatie woont de module onder "Internetdata verwerken", direct naast binascii, en die plaatsing is geen toeval. b64decode is een dunne omhulling die het alfabet vertaalt (als je altchars doorgeeft) en daarna de C-functie binascii.a2b_base64 het zware werk laat doen. Daarom is de functie snel, en daarom hebben zijn foutmeldingen die frisse, onsentimentele smaak van C.
Het werkpaard: b64decode
Hier is het volledige contract, kort genoeg om in je hoofd te houden. De functie neemt een bytes-achtig object of een ASCII-tekenreeks, een optionele alfabetwissel van twee tekens, en een validatievlag. Hij levert een bytes-object terug. Bij falen roept hij binascii.Error op, een subklasse van ValueError voor het geval je ooit een hele familie excepties in één keer wilt vangen:
import base64
data = base64.b64decode("Zm9vYmFy")
print(data)
# b'foobar'
print(type(data))
# <class 'bytes'>
Die laatste regel is de allerbelangrijkste regel van dit artikel. Het resultaat is bytes, geen tekenreeks, en Python houdt je hand precies zo lang vast als het zou moeten: als je het object print, zie je de b'...'-weergave, en als je het probeert aan een tekenreeks te plakken, komt er een TypeError. Het moment waarop je échte tekst wilt is een beslissing die aan jou is, en de tekenset-sectie hieronder behandelt wanneer die beslissing makkelijk is en wanneer het een valkuil is.
Het optionele argument altchars wisselt de + en / van het standaardalfabet om naar een ander paar tekens. Precies die schakelaar geeft het URL-veilige dialect op, en zo is urlsafe_b64decode gebouwd bovenop b64decode. Zelf zult je zelden naar altchars grijpen, maar het is goed om te weten dat het mechanisme er is. Voor alles anders doet de functie gewoon zijn werk, snel, in C.
Standaard soepel, streng op aanvraag
Standaard is b64decode een beleefde vergetel. Elk teken dat niet in het alfabet van 64 tekens staat (en ook niet in jouw altchars) wordt stilletjes weggegooid vóórdat het decoderen begint, en wat overleeft wordt gedecodeerd. Geen waarschuwing, geen melding, geen terugkeerwaarde om te controleren, gewoon een resultaat. Die tolerantie heeft een edele voorouder: sectie 6.8 van RFC 2045 vertelt decoders dat "alle regeleindes of andere tekens die niet in Tabel 1 staan, genegeerd moeten worden", want SMTP wikkelde historisch lange regels om en strooide onderweg losse tekens rond. Een lading die een mailclient, een chatapp of een PDF-kopie is gepasseerd, decodeert vaak zonder enig voorbereidend werk, en dat is een échte superkracht.
Dezelfde goedheid is ook de reden waarom de standaarddecoder nutteloos is om mee te valideren. Sectie 12 van RFC 4648 noemt het risico expliciet: niet-alfabettekens negeren in plaats van de hele codering afwijzen opent een sluik kanaal waarmee informatie kan lekken, en het kan gelijkheidscontroles van tekenreeksen breken, want twee verschillende ingaven kunnen naar dezelfde bytes decoderen. Voor alles wat je niet zelf hebt gecodeerd, geef validate=True door en behandel de exceptie als het antwoord. Hier is het schadeverslag, elke regel reproduceerbaar op elke moderne Python:
| Wat Er In Komt | Soepel (standaard) | validate=True |
|---|---|---|
Zm9vYmFy (een schone lading) |
b'foobar' |
b'foobar' |
Zm9v\r\nYmFy (regeleinde in het midden) |
b'foobar' |
binascii.Error |
Zm9v YmFy (extra spaties) |
b'foobar' |
binascii.Error |
Zm9v!YmFy (een losse uitroepteken) |
b'foobar' |
binascii.Error |
junkZm9vYmFy (een woord vóór de lading) |
b'\x8e\xe9\xe4foobar' |
b'\x8e\xe9\xe4foobar' |
Zm9v=YmFy (een opvulling in het midden) |
b'foobar' |
binascii.Error |
=Zm9v (opvulling vooraan) |
b'foo' |
binascii.Error |
==== (vier opvullingstekens, geen data) |
b'' |
binascii.Error |
(lege invoer) |
b'' |
b'' |
Kijk hoe de soepele kolom zijn stille werk doet. De rij die mensen het eerst verbaast is de met een woord vooraan: alle vier letters van junk zitten toevallig in het Base64-alfabet, dus de "afval" decodeert naar drie échte bytes en wordt met een serieuze blik aan je lading geplakt. De strenge modus is hier geen redder, want de invoer is inderdaad geldige Base64; het zijn de andere rijen die plat worden geweigerd, en de weigeringen hebben precies één vorm: een binascii.Error met een van een handvol onvergetelijke meldingen:
Incorrect padding- de lengte is na weggooien geen veelvoud van vier, of de laatste groep is te kort. Een tekenreeks alsZm9vYmEzonder enig opvullingsteken komt hier terecht.Invalid base64-encoded string: number of data characters (N) cannot be 1 more than a multiple of 4- de invoer is precies één teken te kort voor de volgende groep. Dit is de klassieke vingerafdruk van een afgekapt of via de plakband overgenomen lading.Only base64 data is allowed- een niet-alfabetteken heeft het overleefd tot in de strenge modus, en één enkel regeleinde telt ook al.Excess padding not allowed- opvulling in het midden van de tekenreeks, of meer opvullingstekens dan de laatste groep toelaat.Leading padding not allowed- de tekenreeks begint met=.- En één uit een andere familie:
ValueError: string argument should contain only ASCII characters, die je krijgt als je een tekenreeks doorgeeft met niet-ASCII letters erin. Tekenreeksen worden geaccepteerd, maar alleen ASCII-tekenreeksen.
Achter de schermen is validate=True helemaal geen afzonderlijk codepad. De module geeft de vlag door naar binascii.a2b_base64 als haar strict_mode-parameter, de strenge check die in Python 3.11 aan binascii is toegevoegd. Dat geeft je een directe greep als je strengheid wilt zonder via de base64-laag te gaan:
import binascii
line = b"Zm9vYmFy"
print(binascii.a2b_base64(line, strict_mode=True))
# b'foobar'
Eén eigenaardigheid om vast te pinnen vóórdat je de strenge modus blind vertrouwt: hij weigert zelfs één enkel afsluitend regeleinde, dus een MIME-omwikkelde blok is een klus voor de soepele route of voor decodebytes, niet voor validate=True. Houd de strenge route voor data waarvan je verwacht dat ze perfect schoon zijn, zoals een vers geslagen token rechtstreeks uit je eigen code.
base64url, het alfabet dat in URLs past
Het standaardalfabet heeft twee tekens die URLs haten. Het +-teken wordt door elke formulierdecoder als een spatie gelezen, en het /-teken is gereserveerd voor pad-scheidingstekens. Sectie 5 van RFC 4648 definieert het neefdialect, waarin + wordt tot - en / tot _, en waarin de opvulling wordt weggegooid wanneer de datalengte uit de context bekend is. De RFC geeft de variant zelfs een officiële naam, base64url, en benadrukt dat je hem niet zomaar "base64" moet noemen. Je ontmoet hem het vaakst binnen JSON Web Tokens, waar elk deel van het token base64url is zonder opvulling, en hij duikt ook op in OAuth-tokens en API-cursorparameters.
Python levert een eigen functie voor hem, urlsafe_b64decode. Die vertaalt de streepjes en lage streepjes terug naar plussen en schuine strepen en decodeert daarna, maar hij vult voor je niet opnieuw aan. Invoer zonder opvulling is de normale situatie bij JWT's, dus komt de wiskundige regel als eerste, en het is dezelfde regel die bibliotheken als PyJWT onder de motorkap gebruiken:
import base64
segment = "Zm9vYmE"
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'
De expressie "=" * (-len(segment) % 4) lijkt een truc, maar het is het hele werk: hij geeft nul, één of twee opvullingstekens op en nooit drie, dus een reeds opgevulde tekenreeks blijft onaangetast. De negatieve modulo is wat hem laat werken voor tekenreeksen van elke lengte, en het is de ene regel Base64-wiskunde die elke Python-ontwikkelaar uiteindelijk minstens één keer typt.
Nu de gevaarlijke verwarring, want de twee alfabetten lijken elkaar genoeg om in de war te raken. Stuur een base64url-tekenreeks door de standaarddecoder en de streepjes en lage streepjes staan simpelweg niet in het standaardalfabet, dus de soepele decoder slurpt ze op en decodeert wat erover is. Voor sommige ladingen is dat een verminkte bytestream; voor anderen is het helemaal niets:
import base64
tricky = base64.urlsafe_b64encode(b"\xfb\xff\xfe")
print(tricky)
# b'-__-'
print(base64.standard_b64decode(tricky))
# b'' - elk teken werd stilletjes weggegooid
print(base64.urlsafe_b64decode(tricky))
# b'\xfb\xff\xfe'
De omgekeerde richting is vergevingsgezind, en dat is wat de verwarring onopgemerkt houdt: urlsafe_b64decode vertaalt eerst zijn alfabet en decodeert daarna soepel, dus hij accepteert graag een tekenreeks in het standaardalfabet met + en / erin. De les is om niet te freubelen. Kies één functie per dialect en hou je eraan, zoals je dat zou doen met een vreemde valuta: geef je yen uit waar de yen geldig is, niet bij het verkeerde wisselkantoor.
De uitvoer is bytes: het tekensetgesprek
Hier is de zin die de helft van de tekensetvragen die mensen met Base64 meenemen beslist: b64decode decodeert bytes, hij decodeert geen tekst. Er is geen tekensetargument, er is geen omzetting, en niets aan de invoer vertelt Python wat de bytes zouden moeten betekenen. De betekenis is iets wat jij uit de context moet aanleveren, en die context is vrijwel altijd één van drie dingen: een kop die dat zegt, een API-contract dat dat zegt, of een magisch getal dat in de bytes zelf verstopt zit.
import base64
raw = base64.b64decode("w6l0w6k=")
print(raw)
# b'\xc3\xa9t\xc3\xa9'
print(raw.decode("utf-8"))
# été
Hetzelfde idee met het verkeerde label is een luide mislukking, en dat is een zegen. Bytes die geen geldige UTF-8 zijn weigeren een tekenreeks te worden, en de exceptie vertelt je precies welke byte de boosdoener is:
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
Drie vuistregels houden deze sectie uit de horrorshow. Eén: als de gedecodeerde data JSON is, hoef je helemaal niet handmatig te decoderen, want json.loads accepteert sinds Python 3.6 bytes direct en detecteert UTF-8, UTF-16 en UTF-32 vanzelf. Twee: binair is geen tekst, dus een "gedetecteerde" tekenset voor een PNG is een geluksraad in plaats van een feit; check de bytes in plaats van het label. Drie: als de afzender je de tekenset heeft verteld, geloof de afzender, want een content-type-kop of een API-document wint het van elke detector, elke enkele keer.
Waar gedecodeerde Base64 in Python-code verschijnt
Na verloop van tijd begin je de vormen te herkennen. Hier is de veldgids van de plaatsen waar gedecodeerde Base64 in een Python-applicatie tevoorschijn komt, en het recept van één regel voor elk. De secties die volgen geven de meest voorkomende een volledige behandeling:
| Waar Je Het Vindt | Wat Het Is | Hoe Je Het Leest |
|---|---|---|
| Een JWT | de kop-, lading- en handtekeningdelen (RFC 7519) | splits op de punt, urlsafe_b64decode met het opvulherstel |
Een Authorization-kop |
HTTP Basic-inloggegevens, user:pass (RFC 7617) |
verwijder de Basic-prefix, decodeer, splits bij de eerste dubbele punt |
Een data:-URI |
inline media in HTML of CSS (RFC 2397) | knip bij de eerste komma, decodeer de rest |
| Een e-mailbijlage | een Content-Transfer-Encoding: base64-lichaam (RFC 2045) |
get_payload(decode=True) op het berichtdeel |
| Een e-mailkopwaarde | een =?charset?b?...?= gecodeerd woord (RFC 2047) |
laat het email-pakket het voor je decoderen |
| Een PEM-bestand | een gepantserde sleutel of certificaat (RFC 7468) | verwijder de pantserregels, decodeer het lichaam naar DER |
| Een JSON-API-veld | binair dat als tekenreeks is gesmokkeld | decodeer, behandel daarna het resultaat als bytes, geen tekst |
| Een TEXT-kolom of omgevingsvariabele | binair of JSON opgeslagen op een plek die alleen tekst accepteert | decodeer, parseer of schrijf daarna, met de tekenset waar je het over eens bent |
Een JSON Web Token lezen
Een JWT is drie base64url-stukjes, aan elkaar gekoppeld door punten: een kop, een lading, en een handtekening. De eerste twee zijn gewoon JSON, dus er even naar kijken kost elk één regel, met het opvulherstel uit de sectie hierboven:
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'}
Een opmerking over de reikwijdte, want het telt: een token op deze manier inspecteren is een debughulpmiddel, geen authenticatiemechanisme. Dat de lading leesbaar is, betekent niet dat hij echt is; een aanvaller kan de eerste twee segmenten vervalsen zonder ooit je geheim te kennen. Voor échte verificatie geef je het token aan PyJWT (pip install pyjwt) over, die de handtekening controleert en weigert te decoderen zonder een expliciete lijst van algoritmen:
import jwt
# Een sleutel van minder dan 32 bytes loopt PyJWT's InsecureKeyLengthWarning op (PyJWT 2.11+), een eerlijke knor voor een demo-sleutel.
decoded = jwt.decode(token, "super-secret-key", algorithms=["HS256"])
print(decoded)
# {'sub': '1234567890', 'name': 'John Doe'}
Met een verkeerde sleutel krijg je een exceptie in plaats van een dictionary, en dat is precies het gedrag dat je in productiecodel wilt. En als het token met een verlopen tijdstempel aankwam, gooit PyJWT daar ook een exceptie voor, zodat je de namen van de claims zelf nooit hoeft te onthouden.
Een data-URI openen
Data-URI's plaatsen media direct binnen HTML of CSS, zodat de browser geen tweede verzoek hoeft af te vuren: data:, het mediatype, het woord base64, een komma, en de gecodeerde bytes. De splitsing gebeurt bij de eerste komma, en daar stopt het; alles daarna is een lading in het gewone standaardalfabet:
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'
De acht-byte-PNG-handtekening aan het begin van het resultaat is een goedkoop en vrolijk checkje dat je het juiste ding hebt gedecodeerd. Twee valkuilen verdienen een vermelding. Als de URI van een geplukte pagina of een chatbericht komt, verwijder eerst HTML-entiteiten en losse witruimte, want de soepele decoder vergeeft veel afval en levert je dan een beschadigde afbeelding op in plaats van een fout. En als je onbetrouwbare invoer in bulk decodeert, geef dan validate=True door: een data-URI die de strenge validatie niet haalt, was nooit goed gevormd, en je wilt hem niet op gevoel naar schijf schrijven.
De Authorization-kop kraken
Basic-authenticatie (RFC 7617) is het oudste schema in HTTP, en het verankert nog steeds een verrassend aantal API-integraties, webhooks en CI-pipelines. De client stuurt zijn inloggegevens als user:pass, base64-gecodeerd, achter het woord 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
Let op de partition, want dat is het detail dat je later redden: het wachtwoord mag dubbele punten bevatten, de gebruikers-id niet, en alleen de eerste dubbele punt is de scheider. Eén eerlijke opmerking, want de RFC zelf is hier scherp over: base64 is geen versleuteling. RFC 4648 zegt dat base-codering "visueel verbergt informatie die anders makkelijk te herkennen is, zoals wachtwoorden, maar biedt geen enkele computationele vertrouwelijkheid". Een Basic-kop kan door iedereen gedecodeerd worden die het verkeer ziet, dus behandel het als een gemak voor TLS-beschermde verbindingen, niet als een beveiligingsgrens. Als jij de kop verstuurt, bouwt requests hem voor je met auth=("jane", "pa:ss"), en dat is de moeite waard om te gebruiken zodra de bibliotheek al in je stack zit.
E-mail, de oorspronkelijke klant
Base64 werd in 1993 gestandaardiseerd voor precies één klus: binair over laten leven in e-mail. RFC 2045, de MIME-standaard, definieerde de Content-Transfer-Encoding: base64-lichaamscodering, en het is nog steeds de standaardmethode waarmee bijlagen over het internet reizen. Pythons email-pakket doet het hele werk voor je: het parseert de koppen, het decodeert de =?utf-8?b?...?= gecodeerde woorden die RFC 2047 in kopvelden verstopt, en het base64-decodeert lichamen wanneer je het vraagt:
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'
De get_payload(decode=True)-aanroep leest de Content-Transfer-Encoding-kop en base64-decodeert het lichaam voor je, en ontwijnt onderweg de regels van 76 tekens. Het argument policy=policy.default selecteert de moderne interface van Python 3.6 afwaarts, toen de nieuwe policy-gebaseerde e-mail-API ophield voorlopig te zijn, en dat geeft je gedecodeerde kopwaarden direct uit de doos; de ouderwetse parser werkt nog steeds, maar dan blijf je zitten met handmatig decoderen van gecodeerde woorden. Je daalt alleen af naar decodebytes wanneer je een kale snippet parseert die geen volledig bericht is, zoals een blok dat iemand in een ticket heeft geplakt. Voor multipart-berichten: iterer met iter_attachments() en geef elk deel dezelfde eenregelsbehandeling.
PEM-pantsering en het cryptography-pakket
Een PEM-bestand is een kopregel, wat omwikkelde Base64, en een voetregel, en niets meer. De pantsering is decoratief, de Base64 is het hele verhaal, want het decodeert naar de ruwe DER-structuur eronder. Het cryptography-pakket (pip install cryptography) kan het resultaat direct laden, en dat is de reden waarom het de standaardtool is voor alles wat met certificaten en sleutels te maken heeft:
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 de meeste productiekode doe je pantseren-en-decoderen nooit handmatig: load_pem_x509_certificate accepteert de gepantserde bytes en regelt de Base64-stap voor je onder de motorkap. De manuele route verdiend zijn kost wanneer de DER-bytes al in je handen zitten (een databaskolom, een configuratiebestand, een byte-buffer uit een protocol), of wanneer het blok verpakt in een tekenreeks aankwam en je wilt zien wat erin zit vóórdat je het vertrouwt. Sleutels werken op dezelfde manier, met load_der_private_key die aan de andere kant van dezelfde decode-actie wacht.
Bestanden, magische getallen en de .b64-gewoonte
Decoderen is maar de helft van het werk, de bytes willen meestal een bestand. Het patroon is lezen, decoderen, controleren, schrijven, en die check telt, want een gebroken lading zou anders een stilzwijgend verkeerd bestand opleveren dat je pas weken later ontdekt:
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)
Voor snelle eenmalige omzettingen doet de ouderwetse bestand-naar-bestand-functie de hele reis in een enkele aanroep, omwikkelde regels en al:
import base64
with open("photo.b64", "rb") as src, open("photo.png", "wb") as dst:
base64.decode(src, dst)
Wat heb je net eigenlijk gedecodeerd? De eerste bytes van vrijwel elk gangbaar formaat zijn een vaste handtekening, en omdat Base64 deterministisch is, is de gecodeerde handtekening ook vast. Een van deze voorvoegsels zien is als een kenteken herkennen op afstand:
| Waar De Base64 Op Begint | Wat Het Waarschijnlijk Is |
|---|---|
iVBORw0KGgo |
een PNG-afbeelding |
/9j/ |
een JPEG-afbeelding |
R0lGODlh |
een GIF-afbeelding |
JVBERi0 |
een PDF-document |
UEsDBA== |
een ZIP-archief |
UklGRg== |
een RIFF-container (WAV, WEBP, AVI) |
LS0tLS1CRUdJTg== |
een ASCII-gepantseerd blok ("-----BEGIN ...") |
En doe de grootte-wiskunde terwijl het bestand wordt geschreven, want dat is het getal dat mensen verbaast als de schijf vol komt: coderen blaast data met zo'n derde op, dus een bestand van 300 KB reist als ongeveer 400 KB aan Base64-tekst, en het bestand dat je terug decodeert heeft de kleinere, originele maat. Je schijf, en je geheugen als je het hele bestand tegelijk leest, moeten rekening houden met dat verschil.
Databases, configuratiebestanden en omgevingsvariabelen
Base64 is favoriet om binair (of JSON) te smokkelen via opslag die alleen tekst accepteert: een TEXT-kolom, een waarde in een .ini-bestand, een omgevingsvariabele in een deploy-pipeline. Het decoderrecept is hetzelfde als voor bestanden, dan de schijf:
import base64
import json
stored = "eyJyb2xlIjogImFkbWluIiwicHJvamVjdCI6Im15c2l0ZSJ9"
payload = json.loads(base64.b64decode(stored))
print(payload)
# {'role': 'admin', 'project': 'mysite'}
Twee opmerkingen voor deze hoek van het huis. Wanneer de opgeslagen waarde JSON is, sla de tussenschapsstap .decode("utf-8") over en laat json.loads de bytes direct nemen, want het doet dat sinds Python 3.6. En één eerlijke waarschuwing, want hier zit het duurste misverstand van het hele artikel: Base64 in een omgevingsvariabele of een configuratiebestand is een schild tegen de mens die even naar het bestand kijkt, niet tegen degene die het leest. Als de waarde écht gevoelig is, versleutel het eerst (het cryptography-pakket levert Fernet precies hiervoor) en pas daarna de versleutelde tekst nog in Base64, als je opslag tekst eist.
Als de lading in stukjes aankomt
De standaardbibliotheek heeft geen incrementele Base64-decoder: er is geen update-en-finish-paar, dus streamende data heeft wat eigen administratie nodig. De wiskunde is tegelijk simpel en streng. Vier gecodeerde tekens maken drie bytes, dus je kunt alleen complete groepen van vier tekens decoderen, en je moet de rest meenemen naar het volgende fragment:
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)
Geef hem een socket-buffer, een bestand dat in stukjes van 64 KB wordt gelezen, of een generator van regels zonder regeleindes, en de uitvoer is identiek aan het decoderen van alles in één keer. Als je invoer gegarandeerd schoon en niet-omwikkelde is, houd dan de strengheid vast door elke complete groep met validate=True te decoderen, en onthoud dat de laatste restpartij kan het opvulherstel nodig hebben, en daarom voegt de helper die toe vóór de laatste decode-actie. Dit is dezelfde naadlogica die de encoders aan de andere kant gebruiken, alleen dan met vier tekens in plaats van drie bytes.
Vanaf de opdrachtregel
De base64-module doet dienst als een kleine opdrachtregeltool, en dat is handig wanneer de lading in je terminal ligt in plaats van in je code. Coderen is de standaard; -d (of zijn tweeling, -u) decodeert:
echo -n "hello world" | python3 -m base64
aGVsbG8gd29ybGQ=
echo -n "aGVsbG8gd29ybGQ=" | python3 -m base64 -d
hello world
Hij leest van stdin als je hem geen bestand geeft, of van het bestand dat je noemt, en onder de motorkap is het de ouderwetse bestand-naar-bestand-interface, dus de uitvoer komt omgebroken op 76 tekens met een afsluitend regeleinde per regel. Voor het plakken van een lading in een sessie met de strengheid op het hoogste peil, is de eenregelsversie van de decoder een fijne gewoonte:
import base64
import sys
print(base64.b64decode(sys.stdin.read(), validate=True))
Negen manieren om je eraan te verbranden
Elke Base64-decodebug in Python is een van deze. Bewaar de lijst ergens waar je hem vindt in een paniek, want hij heeft meer namiddagen opgeëist dan elk ander document dat je dit jaar leest. De eerste drie komen met code, want die zitten beter in het geheugen zodra je het wrak hebt gezien:
De ontbrekende opvulling. De meest voorkomende crash van allemaal, meestal omdat een JWT-deel of een API-waarde zonder opvullingstekens aankwam:
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'
De afgeknipte tekenreeks. Wanneer de foutmelding zegt dat het aantal datatekens "cannot be 1 more than a multiple of 4", dan is de lading onderweg afgekapt, of is een kopie-plak-actie een teken aan het einde kwijtgeraakt. Geen hoeveelheid opvulling repareert een tekenreeks waarvan de lengte één modulo vier is, de data is er gewoon niet, en het eerlijke antwoord is om om de lading te vragen.
De stille afval. Soepele modus decodeert wat er overleeft, en gewone Engelse woorden zitten vol met Base64-alfabettekens, dus een los woord vóór de lading wordt échte bytes die aan je data geplakt zitten:
import base64
print(base64.b64decode("junkZm9vYmFy"))
# b'\x8e\xe9\xe4foobar' - drie bytes puur fictie, dan de waarheid
De andere zes hebben helemaal geen code nodig:
- Je decodeerde een base64url-tekenreeks met de standaarddecoder. De streepjes en lage streepjes zitten niet in het standaardalfabet, dus ze verdwenen stilletjes en de lading kwam er kapot uit, of helemaal niet. Gebruik
urlsafe_b64decodemet het opvulherstel. - Je vergeet dat het resultaat bytes is. Het plakken aan een tekenreeks geeft een
TypeError, en het duwen in een JSON-antwoord serialiseert deb'...'-weergave. Roep.decode(encoding)aan op de grens, bewust, met de codering die je écht bedoelt. - Je gaf een niet-ASCII-tekenreeks door. De decoder accepteert tekenreeksen, maar alleen ASCII-tekenreeksen, alles anders is een
ValueError. Als je lading uit een tekstbestand kwam dat met de verkeerde codering werd gelezen, herstel dan het lezen, niet het decoderen. - Je decodeerde twee keer. De data was stroomopwaarts al gedecodeerd, of het was Base64 van Base64, en de tweede ronde veranderde je wachtwoord in zes bytes die geen mens ooit nog zal lezen.
- Je gebruikte de strenge modus op omwikkelde data. Een enkel regeleinde is genoeg om
validate=Truete laten gooien, dus MIME-blokken en PEM-lichamen behoren tot de soepele tools, niet tot de strenge. - Je vertrouwde een opvullingsteken in het midden. In soepele modus wordt een
=overal in de tekenreeks stilletjes weggegooid, dus een beschadigde lading met een verkeerd geplaatst opvullingsteken kan decoderen naar het "juiste" antwoord. Alleen de strenge modus merkt dat, en hij merkt het door te weigeren.
Als je taak is om de poortwachter te zijn, is hier een kleine helper die de twee stemmingen samen aan het werk zet: eerst streng, daarna opvulherstel, en een luide mislukking als het geen van beide helpt:
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'
Merk op dat de helper het alfabet nog steeds vertrouwt dat hem wordt verteld te vertrouwen. Als je invoer mogelijk base64url is, geef hem dan in plaats daarvan aan urlsafe_b64decode. Validatie is een contract, en het contract zegt welk dialect de data in heeft.
Drie decennia van een stille module
De module zit al een kwart eeuw in de standaardbibliotheek, en de meeste tijd zat hij stil. Wanneer hij zich wél verplaatste, waren de verplaatsingen klein maar werkelijk, en ze verklaren enkele "werkt op mijn machine"-verhalen die door oude forums dwalen:
- 1995 - Jack Jansen schreef
base64.pyom om het echte werk aan de C-level-modulebinasciiover te laten. De comment zit nog steeds in het bestand, en die overdracht is vandaag nog steeds van kracht. - 2003, verschenen in Python 2.4 - Barry Warsaw voegde volledige RFC 3548-ondersteuning toe: de
b16,b32enb64-families, plus destandard_*- enurlsafe_*-varianten die je vandaag gebruikt. - Python 3.1 -
encodestringendecodestringwerden afgeschreven ten voordele vanencodebytesendecodebytes, de namen die bleven plakken. - Python 3.3 - de decodefuncties begonnen ASCII-tekenreeksen te accepteren, en daarmee eindigde het tijdperk waarin elke decode-actie begon met een bytes-literal.
- Python 3.4 - elk bytes-achtig object (memoryviews inbegrepen) wordt overal geaccepteerd, en de Base85-neefjes,
a85enb85, sloten aan bij de module. - Python 3.9 - de langdurig afgeschreven
encodestringendecodestringwerden eindelijk verwijderd. Oude tutorials die ze aanroepen hebben een hernoeming van één woord nodig. - Python 3.10 -
b32hexencodeenb32hexdecodearriveerden met het uitgebreide hex-alfabet, het alfabet dat gecodeerde data lexicografisch sorteerbaar houdt. - Python 3.11 -
binascii.a2b_base64kreegstrict_mode, en daar op rijdtvalidate=Trueonder de motorkap op. - Python 3.13 -
z85encodeenz85decodebrachten het Z85-dialect van ZeroMQ de standaardbibliotheek in, en de oudeuu-module werd onder PEP 594 verwijderd met een scherp getoonde opmerking om in plaats daarvanbase64te gebruiken. - Python 3.14 -
b16decodewerd tot zes keer sneller: de validatie draait nu opbytes.translatein plaats van een reguliere expressie, en de module importeert helemaal niet meerre. Ook de importtijd kwam terecht op de lijst van verbeterde modules.
Geen van dit verandert wat de functies doen, en dat is de stille luxe van een module die zo oud is: code die in 2005 Base64 decodeerde, decodeert het in 2026 nog steeds, op dezelfde regel, met hetzelfde resultaat.
Kleine genoegens in de marges
Het serieuze werk is klaar, dus hier zijn de kleine genoegens die de module in zijn marges verstopt:
- De eigen documentatie van de module voert dezelfde demonstratie al meer dan een decennium uit:
b'data to be encoded'gaat erin,b'ZGF0YSB0byBiZSBlbmNvZGVk'komt eruit. Als je de base64-pagina van een willekeurige Python-release uit de laatste twintig jaar hebt gelezen, ben je dit paar eerder al tegengekomen. - Het woord
junkis een perfect geldige Base64-tekenreeks. Alle vier letters zitten in het alfabet, en dat is de reden waarom een los woord aan het begin van een lading drie bytes fictie wordt in plaats van een fout, en waarom de soepele modus zijn bijnaam verdient. urlsafe_b64decodeis per ongeluk tweetalig. Hij vertaalt eerst zijn alfabet en decodeert daarna soepel, dus hij leest ook een tekenreeks in het standaardalfabet met+en/erin. Eén functie, twee dialecten, nul klachten.- De foutmeldingen zijn een stabiel mini-lexicon dat niet is verplaatst sinds de C-implementatie:
Incorrect padding,Only base64 data is allowed,Excess padding not allowed,Leading padding not allowed. Leer ze en je kunt een gebroken lading triagien zonder een enkele regel code uit te voeren. - De lege tekenreeks is de enige invoer die helemaal geen reactie krijgt:
b''erin,b''eruit, in beide stemmingen. Niets in, niets uit, geen alarm. - De docstring van de module noemt nog steeds RFC 3548, de editie van 2003 van de specificatie. RFC 4648 is sinds 2006 de gangbare standaard, en de module volgt die trouw zonder zich te storen aan het bijwerken van de zin.
- Python 2 had geen typemuur aan de decode-zijde: een gewone
strerin, een gewonestreruit. De bytes-hervorming van 2007 in de Python 3-ontwikkeling veranderde dat, en de oude Python 2-tutorials zijn waar de meeste "waarom is mijn decodering kapot"-threads nog steeds naar wijzen.
Dus hier is de hele filosofie in vier regels. Geef validate=True door voor alles wat je niet zelf hebt gecodeerd, en behandel de exceptie als een écht antwoord in plaats van een voorstel. Weet welk dialect je in handen hebt, standaard, base64url of MIME-omwikkelde, want de decoder zegt het je niet; hij raadt alleen maar door wat niet past weg te gooien. Behandel het resultaat als bytes totdat je hebt bewezen dat het tekst is, en vraag daarna wie de tekenset bezat. En onthoud dat de vriendelijkste eigenschap van deze functie, de bereidwilligheid om dingen te decoderen die niet helemaal Base64 zijn, precies die eigenschap is die hem gevaarlijk maakt, dus beslis, bij elke aanroep, hoeveel vertrouwen de invoer heeft verdiend.
Als je op een gegeven moment de andere kant op moet, verse bytes terug wikkelen in die vriendelijke lint van letters voor een token, een bijlage of een inline afbeelding, dan wordt het hele verhaal van b64encode in detail behandeld in het gerelateerde Base64-coderingsartikel onderaan deze pagina. De twee richtingen zijn spiegelbeelden van elkaar, maar elk heeft zijn eigen reeks verrassingen, en deze ken je nu van het hart. Veel plezier met het decoderen.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-codering in Python: een complete gids