Décodage Base64 en Python : un guide complet
Quelque part dans votre code, une chaîne de lettres vient de se poser, et elle ne ressemble pas du tout à du texte : une longue série de A à Z, quelques chiffres, un + ou un / de temps en temps, peut-être un - ou un _, et sans doute un ou deux signes = garés à la fin. Derrière cette chaîne se cache peut-être le payload d'un JWT que votre gateway a rejeté, une image cachée au milieu d'une page HTML, un fichier que quelqu'un vous a envoyé comme pièce jointe .b64, ou un bloc de certificat dans un ticket qui a traversé trois help desks. Votre mission : rendre les octets d'origine, exactement tels qu'ils étaient. Et Python est de très bonne humeur pour ce travail, car tout l'outillage est livré dans la bibliothèque standard depuis des décennies : une ligne import base64 et vous êtes prêt sur toutes les plateformes, sans rien installer et sans rien configurer.
Un petit rappel pendant que vous vous installez, parce que tout le monde en a besoin une fois par an : le Base64 réécrit tous les trois octets de données en quatre caractères tirés d'un alphabet de 64 caractères, et quand le groupe final de trois octets est incomplet, le padding = complète le groupe pour que la sortie arrive toujours par quatre. C'est tout le tour de passe-passe. Ce n'est pas de la compression et ce n'est pas du secret, juste un moyen de laisser le binaire survivre aux canaux qui n'acceptent que du texte. La page d'accueil de ce site détaille le format en profondeur, alphabet et calculs du padding compris, donc nous mettrons notre énergie là où la douleur vit vraiment : le côté Python du décodage, et à garder le résultat honnête.
Trois faits façonnent tout ce qui suit, et ils valent la peine d'être mémorisés avant de lire une ligne de plus. D'abord, le décodeur a deux humeurs : un mode par défaut poli et indulgent, qui jette silencieusement tout ce qu'il ne reconnaît pas, et un mode strict qui refuse ce genre d'entrée d'un bloc. Ensuite, le résultat d'un décodage est toujours un objet bytes, jamais une chaîne, et le moment où vous voulez du vrai texte en sortir est une décision que vous devez prendre délibérément. Troisièmement, il y a deux alphabets presque identiques, le standard et le sûr pour les URL, et les confondre est une façon favorite de perdre des données sans la moindre erreur. Ce guide vous mène au-delà de ces trois-là, pour que la prochaine fois qu'un mur de charabia atterrit dans votre terminal, vous soyez en train de sourire au lieu de plisser les yeux.
Le menu complet du décodage
Ouvrez le module base64 et vous trouverez deux générations d'interfaces installées côte à côte. La moderne, centrée sur b64decode, reconvertit les objets de type bytes (et les chaînes ASCII pures) en bytes, et elle parle les deux dialectes de Base64 définis par la RFC 4648. La version legacy est plus ancienne et orientée fichiers : elle travaille sur des objets fichiers, ne connaît que l'alphabet standard, et a été bâtie autour des lignes de 76 caractères avec retour à la ligne que la RFC 2045, la norme MIME du courrier de 1996, exigeait de la sortie encodée. Vous croiserez les noms legacy dans pas mal de code qui traîne depuis un moment, alors voici le côté décodage complet du menu :
| Fonction | Ce qu'elle fait | Notes |
|---|---|---|
base64.b64decode(s, altchars=None, validate=False) |
le cheval de trait : un blob Base64 de nouveau en bytes bruts | accepte des bytes ou une chaîne ASCII, renvoie toujours des bytes |
base64.standard_b64decode(s) |
le même travail, verrouillé sur l'alphabet standard | pratique quand vous connaissez le dialecte à coup sûr |
base64.urlsafe_b64decode(s) |
lit l'alphabet sûr pour les URL avec - et _ |
celle qui lit les JWT |
base64.decodebytes(s) |
décode une ou plusieurs lignes de Base64 avec retour à la ligne | ajoutée en Python 3.1, le chemin qui plaît à MIME, indulgente |
base64.decode(input, output) |
déverse un fichier Base64 dans un fichier brut | legacy, lit ligne par ligne, indulgente |
base64.b32decode(s, casefold=False) |
décode le cousin plus petit, le Base32 | casefold accepte une entrée en minuscules |
base64.b16decode(s, casefold=False) |
décode le Base16, qui est l'hexadécimal pur et simple | jusqu'à six fois plus rapide en Python 3.14 |
binascii.a2b_base64(s, strict_mode=False) |
la fonction au niveau C qui fait le vrai travail | une prise directe sur la rigueur, avec strict_mode depuis Python 3.11 |
Tout ce qui suit se construit sur la première ligne. Un fait mérite d'être connu avant d'aller plus loin : dans la documentation officielle, le module vit sous « Traitement des données Internet », juste à côté de binascii, et ce placement n'est pas un accident. b64decode est un emballage fin qui traduit l'alphabet (quand vous passez altchars) puis laisse au binascii.a2b_base64 de niveau C le gros du travail. C'est pourquoi la fonction est rapide, et pourquoi ses messages d'erreur ont ce goût net, sans sentimentalisme, du C.
Le cheval de trait : b64decode
Voici le contrat entier, assez court pour le garder en tête. La fonction prend un objet de type bytes ou une chaîne ASCII, un échange de deux caractères d'alphabet en option, et un drapeau de validation. Elle rend un objet bytes. En cas d'échec, elle lève binascii.Error, qui est une sous-classe de ValueError, au cas où vous auriez un jour besoin d'attraper toute une famille d'exceptions d'un coup :
import base64
data = base64.b64decode("Zm9vYmFy")
print(data)
# b'foobar'
print(type(data))
# <class 'bytes'>
Cette dernière ligne est la ligne la plus importante de tout cet article. Le résultat est des bytes, pas une chaîne, et Python vous tient la main exactement aussi loin qu'il le devrait : afficher l'objet vous montre la représentation b'...', et essayer de le coller à une chaîne lève un TypeError. Le moment où vous voulez du vrai texte, la décision est la vôtre, et la section sur les charsets plus bas couvre quand cette décision est facile et quand c'est un piège.
L'argument optionnel altchars échange le + et le / de l'alphabet standard contre une autre paire de caractères. C'est précisément le bouton qui produit le dialecte sûr pour les URL, et c'est ainsi que urlsafe_b64decode est construit par-dessus b64decode. Vous irez rarement chercher altchars vous-même, mais il est bon de savoir que la mécanique est là. Pour le reste, la fonction fait simplement le travail, vite, en C.
Indulgent par défaut, strict sur demande
Par défaut, b64decode est un oublieux poli. Tout caractère qui n'est pas dans l'alphabet de 64 caractères (et pas dans vos altchars) est jeté silencieusement avant que le décodage commence, et tout ce qui survit est décodé. Pas d'avertissement, pas de notification, pas de valeur de retour à vérifier, juste un résultat. Cette tolérance a un ancêtre noble : la section 6.8 de la RFC 2045 dit aux décodeurs que « tous les retours à la ligne ou autres caractères non trouvés dans le tableau 1 doivent être ignorés », parce qu'historiquement SMTP coupait les longues lignes et éparpillait des caractères errants en chemin. Un payload qui a traversé un client de messagerie, une application de chat ou une copie PDF se décode souvent sans la moindre préparation, et c'est un vrai super-pouvoir.
La même gentillesse est aussi la raison pour laquelle le décodeur par défaut est inutile comme validateur. La section 12 de la RFC 4648 expose le risque : ignorer les caractères hors alphabet au lieu de refuser tout l'encodage ouvre un canal couvert qui peut servir à fuiter des informations, et ça peut casser les vérifications d'égalité de chaînes, parce que deux entrées différentes peuvent se décoder en les mêmes bytes. Pour tout ce que vous n'avez pas encodé vous-même, passez validate=True et traitez l'exception comme la réponse. Voici le rapport des dégâts, chaque ligne reproduisible sur n'importe quel Python moderne :
| Ce qui entre | Indulgent (par défaut) | validate=True |
|---|---|---|
Zm9vYmFy (un payload propre) |
b'foobar' |
b'foobar' |
Zm9v\r\nYmFy (retour à la ligne au milieu) |
b'foobar' |
binascii.Error |
Zm9v YmFy (espaces en trop) |
b'foobar' |
binascii.Error |
Zm9v!YmFy (un point d'exclamation errant) |
b'foobar' |
binascii.Error |
junkZm9vYmFy (un mot devant le payload) |
b'\x8e\xe9\xe4foobar' |
b'\x8e\xe9\xe4foobar' |
Zm9v=YmFy (un pad au milieu) |
b'foobar' |
binascii.Error |
=Zm9v (du padding en tête) |
b'foo' |
binascii.Error |
==== (quatre pads, aucune donnée) |
b'' |
binascii.Error |
(entrée vide) |
b'' |
b'' |
Regardez la colonne indulgente faire son travail silencieux. La ligne qui surprend d'abord est celle avec un mot en tête : les quatre lettres de junk se trouvent toutes dans l'alphabet Base64, alors les « déchets » se décodent en trois octets bien réels et se collent à votre payload la mine de rien. Le mode strict n'est pas un sauveur sur cette ligne, parce que l'entrée est vraiment du Base64 valide ; ce sont les autres lignes qui sont refusées d'un bloc, et les refus ont exactement une forme : un binascii.Error portant l'un d'une poignée de messages mémorables :
Incorrect padding- la longueur n'est pas un multiple de quatre après le nettoyage, ou un groupe final est trop court. Une chaîne commeZm9vYmEsans le moindre pad atterrit ici.Invalid base64-encoded string: number of data characters (N) cannot be 1 more than a multiple of 4- l'entrée manque exactement d'un caractère pour le groupe suivant. C'est l'empreinte classique d'un payload tronqué ou copié-collé.Only base64 data is allowed- un caractère hors alphabet a survécu jusqu'au mode strict, et un seul retour à la ligne en compte pour un.Excess padding not allowed- des pads au milieu de la chaîne, ou plus de pads que le groupe final ne l'autorise.Leading padding not allowed- la chaîne commence par=.- Et un de la famille voisine :
ValueError: string argument should contain only ASCII characters, que vous obtenez quand vous passez une chaîne contenant des lettres non ASCII. Les chaînes sont acceptées, mais seulement celles en ASCII.
Derrière les coulisses, validate=True n'est pas du tout un chemin de code à part. Le module transmet le drapeau à binascii.a2b_base64 en tant que son paramètre strict_mode, la vérification stricte qui a été ajoutée à binascii en Python 3.11. Ça vous donne une prise directe quand vous voulez la rigueur sans passer par la couche base64 :
import binascii
line = b"Zm9vYmFy"
print(binascii.a2b_base64(line, strict_mode=True))
# b'foobar'
Une bizarrerie à bien cerner avant de faire confiance au mode strict les yeux fermés : il rejette même un seul retour à la ligne en fin de chaîne, alors un bloc avec retours à la ligne façon MIME est un travail pour le chemin indulgent ou pour decodebytes, pas pour validate=True. Gardez le chemin strict pour des données que vous attendez parfaitement propres, comme un token fraîchement frappé sortant directement de votre propre code.
base64url, l'alphabet qui tient dans les URL
L'alphabet standard a deux caractères que les URL détestent. Le signe + est lu comme un espace par n'importe quel décodeur de formulaire, et le signe / est réservé aux séparateurs de chemin. La section 5 de la RFC 4648 définit le dialecte cousin, où + devient - et / devient _, et où le padding est jeté chaque fois que la longueur des données est connue depuis le contexte. La RFC donne même à la variante un vrai nom, base64url, et insiste pour qu'on ne l'appelle pas simplement « base64 ». Vous la croiserez le plus souvent dans les JSON Web Tokens, où chaque partie du token est en base64url sans padding, et elle pointe aussi le bout de son nez dans les tokens OAuth et les paramètres de curseur d'API.
Python fournit une fonction dédiée pour elle, urlsafe_b64decode. Elle traduit les tirets et les underscores de nouveau en plus et en slashs, puis décode, mais elle ne rajoutera pas le padding à votre place. L'entrée sans padding est le cas normal pour les JWT, alors la ligne d'arithmétique vient en premier, et c'est la même que des bibliothèques comme PyJWT utilisent sous le capot :
import base64
segment = "Zm9vYmE"
padded = segment + "=" * (-len(segment) % 4)
print(base64.urlsafe_b64decode(padded))
# b'fooba'
L'expression "=" * (-len(segment) % 4) a l'air d'un tour de passe-passe, mais c'est tout le travail : elle produit zéro, un ou deux pads, jamais trois, alors une chaîne déjà munie de padding passe sans qu'on y touche. C'est le modulo négatif qui fait que ça marche pour des chaînes de toutes longueurs, et c'est la ligne d'arithmétique Base64 que chaque développeur Python finit par taper au moins une fois.
Maintenant le mélangage dangereux, parce que les deux alphabets se ressemblent assez pour se confondre. Faites passer une chaîne base64url dans le décodeur standard et les tirets et underscores sont tout simplement absents de l'alphabet standard, alors le décodeur indulgent les avale et décode ce qui reste. Pour certains payloads, c'est un flux d'octets cabossé ; pour d'autres, c'est tout bonnement rien :
import base64
tricky = base64.urlsafe_b64encode(b"\xfb\xff\xfe")
print(tricky)
# b'-__-'
print(base64.standard_b64decode(tricky))
# b'' - chaque caractère a été jeté discrètement
print(base64.urlsafe_b64decode(tricky))
# b'\xfb\xff\xfe'
Le sens inverse est indulgent, et c'est ce qui rend le mélangage invisible : urlsafe_b64decode traduit d'abord son alphabet, puis décode en mode indulgent, alors elle acceptera joyeusement une chaîne de l'alphabet standard avec du + et du / dedans. La leçon, c'est de ne pas improviser. C'est de choisir une fonction par dialecte et de s'y tenir, comme avec une monnaie étrangère : dépensez le yen là où le yen est valable, pas dans le mauvais bureau de change.
La sortie est des bytes : la conversation sur les charsets
Voici la phrase qui tranche la moitié des questions sur les charsets que les gens apportent au Base64 : b64decode décode des bytes ; il ne décode pas du texte. Il n'y a pas d'argument charset, il n'y a pas de conversion, et rien dans l'entrée ne dit à Python ce que les octets sont censés signifier. Le sens, c'est quelque chose que vous devez fournir depuis le contexte, et ce contexte est presque toujours l'une de trois choses : un en-tête qui le dit, un contrat d'API qui le dit, ou un numéro magique qui se cache dans les octets eux-mêmes.
import base64
raw = base64.b64decode("w6l0w6k=")
print(raw)
# b'\xc3\xa9t\xc3\xa9'
print(raw.decode("utf-8"))
# été
La même idée avec le mauvais libellé est un échec retentissant, et c'est une chance. Les bytes qui ne sont pas du UTF-8 valide refusent de devenir une chaîne, et l'exception vous dit exactement quel octet a fait tiquer :
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
Trois règles générales empêchent cette section de devenir un spectacle d'horreur. Une : quand les données décodées sont du JSON, vous n'avez pas besoin de décoder manuellement du tout, parce que json.loads accepte des bytes directement depuis Python 3.6 et détecte UTF-8, UTF-16 et UTF-32 tout seul. Deux : le binaire n'est pas du texte, alors un charset « détecté » pour un PNG est un coup de chance plutôt qu'un fait ; vérifiez les octets plutôt que le libellé. Trois : si l'expéditeur vous a dit le charset, croyez l'expéditeur, parce qu'un en-tête content-type ou un document d'API prime sur n'importe quel détecteur, à chaque fois.
Où le Base64 décodé apparaît dans le code Python
Après un moment, vous commencez à reconnaître les formes. Voici le guide de terrain des endroits où le Base64 décodé se pointe dans une application Python, et la recette d'une ligne pour chacun. Les sections suivantes passent en détail les plus courantes :
| Où vous le trouvez | Ce que c'est | Comment le lire |
|---|---|---|
| Un JWT | les parties en-tête, payload et signature (RFC 7519) | coupez au point, urlsafe_b64decode avec le correctif de padding |
Un en-tête Authorization |
les identifiants HTTP Basic, user:pass (RFC 7617) |
enlevez le préfixe Basic, décodez, coupez au premier deux-points |
Une URI data: |
un média intégré dans du HTML ou du CSS (RFC 2397) | coupez à la première virgule, décodez le reste |
| Une pièce jointe de courrier | un corps Content-Transfer-Encoding: base64 (RFC 2045) |
get_payload(decode=True) sur la partie du message |
| Une valeur d'en-tête de courrier | un mot encodé =?charset?b?...?= (RFC 2047) |
laissez le paquet email le décoder pour vous |
| Un fichier PEM | une clé ou un certificat blindés (RFC 7468) | jetez les lignes de blindage, décodez le corps en DER |
| Un champ d'API JSON | du binaire passe en contrebande sous forme de chaîne | décodez, puis traitez le résultat comme des bytes, pas du texte |
| Une colonne TEXT ou une variable d'env | du binaire ou du JSON rangé dans un endroit texte uniquement | décodez, puis parsez ou écrivez, avec le charset convenu |
Lire un JSON Web Token
Un JWT est trois morceaux base64url joints par des points : un en-tête, un payload, et une signature. Les deux premiers sont du JSON pur, alors y jeter un œil tient en une ligne chacun, en utilisant le correctif de padding de la section ci-dessus :
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'}
Une note sur le périmètre, parce que ça compte : inspecter un token de cette façon est un outil de débogage, pas un mécanisme d'authentification. Que le payload soit lisible ne veut pas dire qu'il est authentique ; un attaquant peut forger les deux premiers segments sans jamais connaître votre secret. Pour une vraie vérification, remettez le token à PyJWT (pip install pyjwt), qui vérifie la signature et refuse de décoder sans une liste explicite d'algorithmes :
import jwt
# Une clé de moins de 32 octets déclenche l'InsecureKeyLengthWarning de PyJWT (PyJWT 2.11+), une récrimination méritée pour une clé de démo.
decoded = jwt.decode(token, "super-secret-key", algorithms=["HS256"])
print(decoded)
# {'sub': '1234567890', 'name': 'John Doe'}
Avec une mauvaise clé, vous obtenez une exception au lieu d'un dictionnaire, ce qui est exactement le comportement que vous voulez dans du code de production. Et si le token est arrivé avec un horodatage expiré, PyJWT lève aussi pour ça, alors vous n'aurez jamais à retenir les noms des claims vous-même.
Ouvrir une URI data
Les URI data embarquent des médias directement dans le HTML ou le CSS pour que le navigateur ne déclenche pas une seconde requête : data:, le type média, le mot base64, une virgule, et les octets encodés. La coupure se fait à la première virgule, point final, et tout ce qui suit est un payload tout simple de l'alphabet standard :
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'
La signature PNG de huit octets au début du résultat est une vérification bon marché et joyeuse que vous avez décodé la bonne chose. Deux pièges méritent une mention. Si l'URI vient d'une page scrapée ou d'un message de chat, enlevez d'abord les entités HTML et les espaces en trop, parce que le décodeur indulgent pardonnera pas mal de déchets et vous remettra une image corrompue au lieu d'une erreur. Et si vous décodez des entrées non fiables en gros, passez validate=True : une URI data qui échoue à la validation stricte est une URI data qui n'a jamais été bien formée, et vous ne voulez pas l'écrire sur le disque sur un simple pressentiment.
Craquer l'en-tête Authorization
L'authentification Basic (RFC 7617) est le plus ancien schéma de HTTP, et elle ancre encore un nombre surprenant d'intégrations d'API, de webhooks et de pipelines CI. Le client envoie ses identifiants sous forme de user:pass, encodés en base64, derrière le mot 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
Remarquez partition, parce que c'est le détail qui vous sauvera plus tard : le mot de passe peut contenir des deux-points, l'identifiant utilisateur non, et seul le premier deux-points est le séparateur. Une note honnête, parce que la RFC elle-même est directe là-dessus : base64 n'est pas du chiffrement. La RFC 4648 dit que l'encodage base « masque visuellement des informations autrement facilement reconnaissables, comme des mots de passe, mais ne fournit aucune confidentialité computationnelle ». Un en-tête Basic peut être décodé par n'importe qui qui voit le trafic, alors traitez-le comme une commodité pour des connexions protégées par TLS, pas comme une frontière de sécurité. Quand c'est vous qui envoyez l'en-tête, requests le construit pour vous avec auth=("jane", "pa:ss"), ce qui vaut le coup d'être utilisé chaque fois que la bibliothèque est déjà dans votre stack.
Le courrier, le client d'origine
Le Base64 a été normalisé en 1993 pour exactement un seul travail : faire survivre le binaire au courrier. La RFC 2045, la norme MIME, a défini l'encodage de corps Content-Transfer-Encoding: base64, et c'est encore la façon par défaut dont les pièces jointes voyagent à travers l'internet. Le paquet email de Python fait tout le travail pour vous : il parse les en-têtes, il décode les mots encodés =?utf-8?b?...?= que la RFC 2047 cache dans les champs d'en-tête, et il décode en base64 les corps quand vous lui demandez :
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'
L'appel get_payload(decode=True) lit l'en-tête Content-Transfer-Encoding et décode le corps en base64 pour vous, en déroulant les lignes de 76 caractères en chemin. L'argument policy=policy.default sélectionne l'interface moderne depuis Python 3.6, quand la nouvelle API de courrier basée sur les policies a cessé d'être provisoire, ce qui vous donne des valeurs d'en-tête décodées tout de suite ; le parseur legacy fonctionne toujours, mais vous finissez par décoder les mots encodés à la main. Vous descendez seulement à decodebytes quand vous parsez un extrait brut qui n'est pas un message complet, comme un bloc que quelqu'un a collé dans un ticket. Pour les messages multipart, itérez avec iter_attachments() et donnez à chaque partie le même traitement d'une ligne.
Le blindage PEM et le paquet cryptography
Un fichier PEM est une ligne d'en-tête, un peu de Base64 avec retours à la ligne, et une ligne de pied, et rien de plus. Le blindage est décoratif ; le Base64 est toute l'histoire, parce qu'il se décode en la structure DER brute qui est dessous. Le paquet cryptography (pip install cryptography) peut charger le résultat directement, c'est pourquoi c'est l'outil standard pour tout ce qui touche aux certificats et aux clés :
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
Dans la plupart du code de production, vous ne faites jamais le blindage-puis-décodage à la main : load_pem_x509_certificate accepte les bytes blindés et gère l'étape Base64 pour vous sous le capot. Le chemin manuel paie son prix quand les bytes DER sont déjà entre vos mains (une colonne de base de données, un fichier de config, un tampon d'octets d'un protocole), ou quand le bloc est arrivé enveloppé dans une chaîne et que vous voulez voir ce qu'il y a dedans avant de lui faire confiance. Les clés fonctionnent de la même façon, avec load_der_private_key qui attend de l'autre côté du même décodage.
Fichiers, nombres magiques et l'habitude des .b64
Le décodage n'est que la moitié du travail ; les octets veulent d'habitude un fichier. Le schéma est lire, décoder, vérifier, écrire, et la vérification compte parce qu'un payload cassé produira sinon un fichier faux en silence que vous découvrirez des semaines plus tard :
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)
Pour les conversions rapides et uniques, la fonction legacy fichier-vers-fichier fait tout le trajet en un seul appel, lignes avec retours à la ligne et tout inclus :
import base64
with open("photo.b64", "rb") as src, open("photo.png", "wb") as dst:
base64.decode(src, dst)
Qu'avez-vous décodé, au fait ? Les premiers octets de presque tous les formats courants sont une signature fixe, et parce que le Base64 est déterministe, la signature encodée est fixe aussi. Voir un de ces préfixes est comme reconnaître une plaque d'immatriculation à distance :
| Le Base64 commence par | C'est probablement |
|---|---|
iVBORw0KGgo |
une image PNG |
/9j/ |
une image JPEG |
R0lGODlh |
une image GIF |
JVBERi0 |
un document PDF |
UEsDBA== |
une archive ZIP |
UklGRg== |
un conteneur RIFF (WAV, WEBP, AVI) |
LS0tLS1CRUdJTg== |
un bloc blindé ASCII (« -----BEGIN ... ») |
Et faites le calcul de taille pendant que le fichier s'écrit, parce que c'est le nombre qui surprend quand le disque se remplit : l'encodage gonfle les données d'environ un tiers, alors un fichier de 300 KB voyage comme environ 400 KB de texte Base64, et le fichier que vous re-décodez fait la taille plus petite, l'originale. Votre disque, et votre mémoire si vous lisez tout le fichier d'un coup, doivent budgéter la différence.
Bases de données, fichiers de config et variables d'environnement
Le Base64 est un chouchou pour passer du binaire (ou du JSON) en contrebande à travers des stockages qui n'acceptent que du texte : une colonne TEXT, une valeur dans un fichier .ini, une variable d'environnement dans un pipeline de déploiement. La recette de décodage est la même que pour les fichiers, sans le disque :
import base64
import json
stored = "eyJyb2xlIjogImFkbWluIiwicHJvamVjdCI6Im15c2l0ZSJ9"
payload = json.loads(base64.b64decode(stored))
print(payload)
# {'role': 'admin', 'project': 'mysite'}
Deux notes pour ce coin de la maison. Quand la valeur stockée est du JSON, sautez l'étape intermédiaire .decode("utf-8") et laissez json.loads prendre les bytes directement, parce qu'il le fait depuis Python 3.6. Et un avertissement honnête, parce que c'est là que vit le malentendu le plus cher de tout l'article : le Base64 dans une variable d'env ou un fichier de config est un bouclier contre l'humain qui jette un œil au fichier, pas contre celui qui le lit. Si la valeur est vraiment sensible, chiffrez-la d'abord (le paquet cryptography fournit Fernet exactement pour ça) et seulement après, mettez le texte chiffré en Base64 si votre stockage exige du texte.
Quand le payload arrive en morceaux
La bibliothèque standard n'a pas de décodeur Base64 incrémental : il n'y a pas de paire update-et-finish, alors les données en flux ont besoin d'un peu de comptabilité de votre part. L'arithmétique est simple et stricte en même temps. Quatre caractères encodés font trois octets, alors vous ne pouvez décoder que des groupes complets de quatre caractères, et vous devez porter le reste sur le morceau suivant :
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)
Alimentez-le avec un tampon de socket, un fichier lu par morceaux de 64 Ko, ou un générateur de lignes avec les retours à la ligne retirés, et la sortie est identique au décodage du tout en une seule fois. Si votre entrée est garantie propre et sans retours à la ligne, gardez la rigueur en décodant chaque groupe complet avec validate=True, et rappelez-vous que le dernier reste peut avoir besoin du correctif de padding, c'est pourquoi l'aide l'ajoute avant le dernier décodage. C'est la même logique de couture que les encodeurs utilisent de l'autre côté, sauf qu'avec quatre caractères au lieu de trois octets.
Depuis la ligne de commande
Le module base64 sert aussi de minuscule outil en ligne de commande, ce qui est pratique quand le payload est assis dans votre terminal au lieu de votre code. L'encodage est le comportement par défaut ; -d (ou son jumeau, -u) décode :
echo -n "hello world" | python3 -m base64
aGVsbG8gd29ybGQ=
echo -n "aGVsbG8gd29ybGQ=" | python3 -m base64 -d
hello world
Il lit depuis stdin quand on ne lui donne pas de fichier, ou depuis le fichier que vous nommez, et sous le capot c'est l'interface legacy fichier-vers-fichier, alors la sortie arrive coupée à 76 caractères avec un retour à la ligne en fin de chaque ligne. Pour coller un payload dans une session avec la rigueur à fond, la version en une ligne du décodeur est une belle habitude :
import base64
import sys
print(base64.b64decode(sys.stdin.read(), validate=True))
Neuf façons de se brûler
Chaque bug de décodage Base64 en Python est l'un de ceux-ci. Gardez la liste quelque part où vous la trouverez en cas de panique, parce qu'elle a rattrapé plus d'après-midis que tout autre document unique que vous lirez cette année. Les trois premiers viennent avec du code, parce qu'ils sont plus faciles à retenir une fois que vous avez vu la débâcle :
Le padding manquant. Le plantage le plus commun de tous, d'habitude parce qu'une partie de JWT ou une valeur d'API est arrivée sans ses pads :
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'
La chaîne tronquée. Quand l'erreur dit que le nombre de caractères de données « ne peut pas être 1 de plus qu'un multiple de 4 », le payload a été coupé en transit, ou un copier-coller a perdu un caractère à la fin. Aucune quantité de padding ne répare une chaîne dont la longueur est un modulo quatre ; les données tout simplement ne sont pas là, et la réponse honnête est de demander le payload encore une fois.
Le déchet silencieux. Le mode indulgent décode tout ce qui survit, et les mots anglais courants regorgent de lettres de l'alphabet Base64, alors un mot errant devant le payload devient de vrais octets collés à vos données :
import base64
print(base64.b64decode("junkZm9vYmFy"))
# b'\x8e\xe9\xe4foobar' - trois octets de pure fiction, puis la vérité
Les six autres n'ont besoin d'aucun code :
- Vous avez décodé une chaîne base64url avec le décodeur standard. Les tirets et underscores ne sont pas dans l'alphabet standard, alors ils ont disparu silencieusement et le payload est sorti cabossé, ou vide. Utilisez
urlsafe_b64decodeavec le correctif de padding. - Vous avez oublié que le résultat est des bytes. Le coller à une chaîne lève un
TypeError, et le pousser dans une réponse JSON sérialise la représentationb'...'. Appelez.decode(encoding)à la frontière, délibérément, avec l'encodage que vous voulez vraiment. - Vous avez passé une chaîne non ASCII. Le décodeur accepte les chaînes, mais seulement celles en ASCII ; tout le reste est un
ValueError. Si votre payload est sorti d'un fichier texte lu avec le mauvais encodage, réparez la lecture, pas le décodage. - Vous avez décodé deux fois. Les données étaient déjà décodées en amont, ou c'était du Base64 de Base64, et le second passage a transformé votre mot de passe en six octets qu'aucun humain ne relira jamais.
- Vous avez utilisé le mode strict sur des données avec retours à la ligne. Un seul retour à la ligne suffit pour faire jeter
validate=True, alors les blocs MIME et les corps PEM appartiennent aux outils indulgents, pas à celui strict. - Vous avez fait confiance à un pad au milieu. En mode indulgent, un
=n'importe où dans la chaîne est jeté silencieusement, alors un payload corrompu avec un pad mal placé peut se décoder à la réponse « juste ». Seul le mode strict le remarque, et il le remarque en refusant.
Si votre métier est de faire le portier, voici une petite aide qui met les deux humeurs à travailler ensemble : strict d'abord, correctif de padding second, et un échec retentissant quand l'un comme l'autre n'aident pas :
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'
Notez que l'aide fait toujours confiance à l'alphabet qu'on lui dit de croire. Si votre entrée risque d'être base64url, alimentez plutôt urlsafe_b64decode. La validation est un contrat, et le contrat dit dans quel dialecte sont les données.
Trois décennies d'un module silencieux
Le module est dans la bibliothèque standard depuis un quart de siècle, et la plupart du temps il est resté assis. Quand il a bougé, les coups étaient petits mais réels, et ils expliquent quelques histoires de « ça marche sur ma machine » qui flottent dans les vieux forums :
- 1995 - Jack Jansen a réécrit
base64.pypour déléguer le vrai travail au modulebinasciide niveau C. Le commentaire est toujours dans le fichier, et la délégation est toujours vraie aujourd'hui. - 2003, livré dans Python 2.4 - Barry Warsaw a ajouté le support complet de la RFC 3548 : les familles
b16,b32etb64, plus les variantesstandard_*eturlsafe_*que vous utilisez aujourd'hui. - Python 3.1 -
encodestringetdecodestringsont devenues non recommandées au profit d'encodebytesetdecodebytes, les noms qui ont collé. - Python 3.3 - les fonctions de décodage ont commencé à accepter des chaînes ASCII, mettant fin à l'ère où chaque décodage commençait par un littéral bytes.
- Python 3.4 - n'importe quel objet de type bytes (memoryviews comprises) est accepté partout, et les cousins Base85,
a85etb85, ont rejoint le module. - Python 3.9 -
encodestringetdecodestring, non recommandées depuis longtemps, ont enfin été retirées. Les vieux tutos qui les appellent ont besoin d'un renommage d'un mot. - Python 3.10 -
b32hexencodeetb32hexdecodesont arrivés avec l'alphabet hexadécimal étendu, celui qui garde les données encodées triables lexicographiquement. - Python 3.11 -
binascii.a2b_base64a gagnéstrict_mode, c'est là-dessus quevalidate=Trueroule sous le capot. - Python 3.13 -
z85encodeetz85decodeont apporté le dialecte Z85 de ZeroMQ dans la bibliothèque standard, et le module antiqueuua été retiré sous PEP 594 avec une note cinglante d'utiliserbase64à la place. - Python 3.14 -
b16decodeest devenu jusqu'à six fois plus rapide : sa validation tourne maintenant surbytes.translateau lieu d'une expression régulière, et le module n'importe plusredu tout. Son temps d'importation est aussi entré dans la liste des modules améliorés.
Aucun de ça ne change ce que font les fonctions, et c'est le luxe discret d'un module aussi vieux : du code qui décodait du Base64 en 2005 le décode encore en 2026, sur la même ligne, avec le même résultat.
Délices des marges
Le travail sérieux est fait, alors voici les petits délices que le module cache dans ses marges :
- La propre documentation du module a fait la même démonstration depuis plus d'une décennie :
b'data to be encoded'entre,b'ZGF0YSB0byBiZSBlbmNvZGVk'sort. Si vous avez lu la page base64 de n'importe quelle sortie Python dans les vingt dernières années, vous avez déjà croisé ce duo. - Le mot
junkest une chaîne Base64 parfaitement valide. Ses quatre lettres sont toutes dans l'alphabet, c'est pourquoi un mot errant au début d'un payload devient trois octets de fiction au lieu d'une erreur, et c'est pourquoi le mode indulgent a mérité son surnom. urlsafe_b64decodeest bilingue par accident. Elle traduit d'abord son alphabet, puis décode en mode indulgent, alors elle lit aussi une chaîne de l'alphabet standard avec du+et du/dedans. Une fonction, deux dialectes, zéro réclamation.- Les messages d'erreur sont un mini-lexique stable qui n'a pas bougé depuis l'implémentation C :
Incorrect padding,Only base64 data is allowed,Excess padding not allowed,Leading padding not allowed. Apprenez-les et vous pourrez trier un payload cassé sans exécuter la moindre ligne de code. - La chaîne vide est la seule entrée qui n'obtient aucune réaction du tout :
b''entre,b''sort, dans les deux humeurs. Rien dedans, rien dehors, aucune alarme. - La docstring du module cite encore la RFC 3548, l'édition 2003 de la spécification. La RFC 4648 est la norme en vigueur depuis 2006, et le module la suit fidèlement sans se donner la peine de mettre à jour la phrase.
- Python 2 n'avait aucun mur de types côté décodage : un
strsimple entre, unstrsimple sort. La grande refonte bytes de 2007 du développement de Python 3 a changé ça, et les vieux tutos Python 2 sont là où pointent la plupart des fils de discussion « pourquoi mon décodage est cassé ».
Alors voici toute la philosophie en quatre règles. Passez validate=True pour tout ce que vous n'avez pas encodé vous-même, et traitez l'exception comme une vraie réponse plutôt que comme une suggestion. Sachez dans quel dialecte vous tenez, standard, base64url ou avec retours à la ligne façon MIME, parce que le décodeur ne vous le dira pas ; il ne fera que deviner en jetant tout ce qui ne rentre pas. Traitez le résultat comme des bytes jusqu'à ce que vous ayez prouvé que c'est du texte, et demandez alors qui possédait le charset. Et rappelez-vous que la caractéristique la plus amicale de cette fonction, la volonté de décoder des choses qui ne sont pas tout à fait du Base64, est la même caractéristique qui la rend dangereuse, alors décidez, à chaque appel, combien de confiance l'entrée a méritée.
Si un jour vous devez aller dans l'autre sens, enrouler de frais octets de nouveau dans ce ruban amical de lettres pour un token, une pièce jointe ou une image en ligne, toute l'histoire de b64encode est couverte en détail dans l'article lié sur l'encodage Base64 en bas de cette page. Les deux sens sont des images miroir, mais chacun a son propre lot de surprises, et vous connaissez maintenant celui-ci par cœur. Bon décodage.
Dernière mise à jour : 2026-09-08
Article associé : Encodage Base64 en Python : un guide complet