Vous avez à traiter le format Base64 ? Alors ce site est parfait pour vous ! Utilisez notre outil en ligne super pratique pour encoder ou décoder vos données.

Encodage Base64 en PHP : un guide complet

Vous avez des données qui doivent survivre à un canal qui ne les aime pas. Un blob binaire qui doit se poser dans un champ JSON. Une image qui doit vivre dans une balise HTML. Un certificat qui a sa place dans un fichier de configuration. Un token qui va voyager dans les URL, les headers et les cookies. Voici la vie quotidienne de l'encodage Base64 : il réécrit chaque groupe de trois octets de données brutes sous forme de quatre caractères d'un alphabet de 64 caractères, avec un ou deux signes = qui terminent la queue, pour que le résultat soit du texte brut que n'importe quoi peut porter. La page d'accueil de ce site explique le format en détail, donc cet article se concentre sur ce que PHP vous donne, ce qu'il décide en silence à votre place, et où sont les pièges.

Côté PHP, l'histoire commence par une bonne nouvelle : base64_encode() vit dans le cœur depuis PHP 4, elle prend un argument, elle renvoie toujours une chaîne, et elle ne peut pas échouer. Pas de mode strict, pas de chemin d'erreur, pas de configuration. L'encodage est déterministe : les mêmes octets produisent toujours les mêmes caractères. Votre travail de développeur n'est pas de faire fonctionner la fonction, c'est de faire se comporter le monde qui l'entoure : choisir le bon alphabet pour la destination, ajouter les bons retours à la ligne, convertir le bon jeu de caractères, et porter la facture de taille de 33 % les yeux ouverts. (Base64 gonfle normalement les données d'environ un tiers, quatre caractères pour trois octets d'entrée ; gardez ça au fond de la tête, parce que ça revient sans cesse.)

À la fin de cet article, vous saurez produire toutes les saveurs de Base64 qu'un développeur PHP rencontre vraiment : sortie sur une seule ligne, e-mail habillé en MIME, clés habillées en PEM, tokens sûrs pour les URL, et data URIs, plus les astuces de streaming pour quand les données sont trop grosses pour tenir en mémoire.

Une fonction, zéro option

L'API entière, telle que la rapporte le PHP moderne :

base64_encode(string $string): string

Relisez une fois. Un paramètre, une valeur de retour, pas de flags. Le manuel le décrit comme du base64 MIME, « conçu pour permettre aux données binaires de survivre au transport à travers des couches de transport qui ne sont pas propres en 8 bits, comme les corps de mail ». Notez ce que cette formulation ne promet pas : pas de retours à la ligne, pas d'habillage, pas d'avis sur l'endroit où vivra la sortie. La fonction émet une seule longue ligne, et l'habillage que la destination veut, c'est votre travail avec un second appel. Depuis PHP 8.0, la signature porte des types natifs ; depuis PHP 8.1, passer null provoque un avertissement de non-recommandation, donc fusionnez d'abord toute valeur nullable avec ''.

La taille de la sortie suit un motif fixe que vous pouvez prédire avant d'appeler :

Octets d'entrée Caractères de sortie Padding
0 0 aucun
1 4 deux =
2 4 un =
3 4 aucun
3,000,000 4,000,000 aucun
100,000 133,336 deux =

Le motif est quatre caractères pour chaque groupe complet de trois octets, plus un groupe partiel final bourré d'un ou deux signes =. Une conséquence à connaître : un octet et trois octets produisent tous les deux quatre caractères, donc la longueur encodée cache la taille exacte de l'entrée. Vous pouvez l'estimer (diviser par quatre, multiplier par trois, soustraire les paddings) mais vous ne pouvez pas la lire exactement.

Où vont les retours à la ligne

Comme base64_encode() n'habille jamais tout seul, la décision d'habillage est un problème de destination. En pratique, il y a trois réponses.

Aucun retour à la ligne. La sortie brute de la fonction, exactement une ligne. C'est ce que vous voulez pour les URL, les payloads JSON, les headers, les valeurs de base de données, et tout ce qui fait qu'un retour à la ligne serait un bug. C'est aussi ce que la plupart des gens entendent par « donne-moi juste le Base64 ».

Habillage MIME : 76 caractères plus CRLF. La convention e-mail de RFC 2045, section 6.8 : les lignes encodées ne doivent pas dépasser 76 caractères, et les décodeurs doivent ignorer les retours à la ligne. L'appel compagnon classique est chunk_split(), que le manuel associe à base64_encode() dans sa liste « See Also » pour exactement cette raison :

$binary = file_get_contents('/var/www/uploads/report.pdf');
$wrapped = chunk_split(base64_encode($binary), 76, "\r\n");

Habillage PEM : 64 caractères plus LF. Les clés et les certificats utilisent la convention plus ancienne Privacy-Enhanced Mail (RFC 1421) : des lignes de 64 caractères plus courtes. Même outil, chiffres différents :

$der = file_get_contents('/etc/ssl/raw-key.der');
$armor = "-----BEGIN PRIVATE KEY-----\n"
  . chunk_split(base64_encode($der), 64, "\n")
  . "-----END PRIVATE KEY-----\n";

Un piège de chunk_split() s'applique aux deux saveurs habillées : la fonction ajoute le séparateur à la fin du résultat même quand la longueur de l'entrée est un multiple exact de la longueur de ligne. Si un consommateur en aval trébuche sur une ligne vide finale, c'est pourquoi ; un appel rtrim() sur le séparateur corrige. Notez aussi l'asymétrie qui vous sauvera un jour : les décodeurs ignorent entièrement les retours à la ligne, donc un payload habillé en MIME et un non habillé se décodent en les mêmes octets. L'habillage est une courtoisie envers les outils à base de lignes et les humains, pas une différence de sens.

Rendre la sortie sûre pour les URL

L'alphabet standard inclut + et /, et les deux posent problème hors d'un fichier texte. Un + dans une chaîne de requête encodée en formulaire devient une espace avant que votre application ne le voie, et / est un séparateur de chemin dans les URL. Les noms de fichiers et les tokens ont leurs propres griefs. RFC 4648, section 5, résout ça avec l'alphabet sûr pour les URL et les noms de fichiers : + devient -, / devient _, et le padding = de la fin est d'habitude retiré. Le RFC tient à ce que cela « ne doit pas être considéré comme le même que l'encodage base64 », donc traitez-le comme un format distinct, communément appelé base64url.

Le produire, ce sont deux opérations sur les chaînes :

function base64url_encode(string $data): string
{
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
var_dump(base64url_encode("hi>?there\x00bin")); // string(18) "aGk-P3RoZXJlAGJpbg"

Quand l'utiliser : les parties de JSON Web Token, les paramètres de state et nonce OAuth, les identifiants d'API que vous mettez dans les chemins d'URL, et tout ce qui sera copié dans une barre d'adresse ou un nom de fichier. Quand ne pas l'utiliser : les corps d'e-mail, l'armure PEM, et partout où un consommateur de l'alphabet standard est de l'autre côté, parce que - et _ ne sont pas dans leur vocabulaire. Et ne mélangez pas les deux alphabets en silence : un token encodé sûr pour les URL doit se décoder sûr pour les URL, partout, pour toujours. C'est toute la règle d'interopérabilité du base64url.

Unicode et les octets que vous vouliez

Les chaînes PHP sont des séquences d'octets, et base64_encode() va encoder n'importe quels octets qu'on lui donne sans demander ce qu'ils veulent dire. C'est une qualité, jusqu'au jour où votre « texte » n'est pas en fait l'encodage que vous croyez. L'échec classique : une chaîne qui a l'air de l'UTF-8 dans votre éditeur mais qui est arrivée d'une source legacy en Windows-1252. Encodez ces octets tels quels, et le récepteur, qui va décoder et supposer de l'UTF-8, obtient du mojibake au lieu de vos lettres accentuées.

La correction est de normaliser avant d'encoder, avec l'extension mbstring (elle est livrée avec la source de PHP mais pas activée par défaut) :

$fromLegacy = "caf\xE9 au lait"; // octets Windows-1252 : le é est 0xE9
$utf8 = mb_convert_encoding($fromLegacy, 'UTF-8', 'Windows-1252');
$encoded = base64_encode($utf8);
var_dump($utf8); // string(13) "café au lait" : le é fait maintenant deux octets UTF-8

Si la source est déjà en UTF-8, vous pouvez sauter la conversion, et un contrôle de santé pas cher, c'est mb_check_encoding($utf8, 'UTF-8'). Une phrase de conseil : n'essayez jamais de « réparer » une chaîne Base64 déjà encodée en la ré-encodant comme du texte. C'est le piège du double encodage de la section des pièges ci-dessous, et c'est le bug Base64 le plus courant dans les bases de code PHP.

Fichiers, blobs et la convention .b64

Le travail d'encodage le plus direct : un fichier devient du texte. Les chaînes PHP sont des octets, donc il n'y a pas de « mode binaire » dont se soucier ; file_get_contents() vous tend les octets exacts et base64_encode() vous tend le texte exact :

$binary = file_get_contents('/var/www/uploads/photo.png');
$encoded = base64_encode($binary);
file_put_contents('/var/www/uploads/photo.png.b64', $encoded);
var_dump(strlen($binary), strlen($encoded)); // la facture de 33 %, à chaque fois

Deux habitudes gardent ça en sécurité. Premièrement, sachez ce que vous encodez. La classe finfo (l'extension fileinfo, fournie avec les builds PHP standards) vous dit le type réel depuis les octets, pas depuis le nom de fichier :

$mime = (new finfo(FILEINFO_MIME_TYPE))->file('/var/www/uploads/photo.png');
var_dump($mime); // string(9) "image/png"

Deuxièmement, pensez à la facture de taille quand vous planifiez le stockage : une image de 500 Ko devient un fichier texte de 670 Ko, et une vidéo de 1 Go devient un fichier texte de 1,33 Go. C'est pour ça que la section grandes données ci-dessous existe.

Data URIs : mettre une image dans la page

Une data URI intègre le payload directement dans l'URL, donc aucune seconde requête n'est nécessaire pour le récupérer. RFC 2397 définit la forme : data:, un type média facultatif, un flag ;base64 facultatif, une virgule, et les données. Pour les médias binaires comme les images, le flag est présent, donc le payload est exactement ce que base64_encode() a produit :

function data_uri_for(string $path): string
{
  $mime = (new finfo(FILEINFO_MIME_TYPE))->file($path);
  $payload = base64_encode(file_get_contents($path));
  return 'data:' . $mime . ';base64,' . $payload;
}
$uri = data_uri_for('/var/www/uploads/avatar.png');
echo '<img src="' . $uri . '" alt="avatar" />' . "\n";

Pourquoi du Base64 ici ? Parce qu'une URI ne peut pas contenir en sécurité des octets bruts ou des virgules, et que l'alphabet Base64 n'a besoin d'aucun échappement. Les compromis sont réels, quand même. Le payload encodé est environ 33 % plus gros que le fichier, ce qui rend le document HTML lui-même plus gros. Les navigateurs ne mettront pas en cache une data URI comme ils mettent en cache une URL de fichier, donc chaque visite de page re-télécharge les octets. Et le RFC lui-même dit que les data URIs ne sont utiles que pour de courtes valeurs ; les vieux parseurs HTML avaient des limites dures sur la longueur d'attribut, et les navigateurs modernes, bien que bien plus généreux, n'aiment toujours pas les mégaoctets dans une balise. Utilisez-les pour les avatars, les icônes et les petites graphiques inline ; utilisez de vrais fichiers pour tout le reste.

JWTs et tokens d'API

Les JSON Web Tokens sont le client vedette du Base64 dans les API modernes, et ils utilisent le dialecte sûr pour les URL et sans padding de la section ci-dessus. Selon RFC 7519, un JWT compact est trois parties base64url séparées par des points : header, payload, signature. Le header et le payload sont du JSON brut ; la signature est des octets bruts. En construire un à la main est un joli moyen de voir chaque pièce mobile :

function base64url_encode(string $data): string
{
  return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}
$header = base64url_encode(json_encode(['alg' => 'HS256', 'typ' => 'JWT']));
$payload = base64url_encode(json_encode([
  'sub' => '1234567890',
  'name' => 'John Doe',
  'iat' => 1516239022,
]));
$signingInput = $header . '.' . $payload;
$signature = base64url_encode(hash_hmac('sha256', $signingInput, $secret, true));
$token = $signingInput . '.' . $signature;

Deux choses à remarquer. La signature est l'encodage base64url d'octets HMAC bruts, c'est pour ça que hash_hmac() est appelé avec true pour une sortie brute. Et le header et le payload sont lisibles par n'importe qui, ce qui est voulu par conception : un JWT est un billet signé, pas un secret. En production, vous ne bricolez pas la signature ni la vérification vous-même. Le package de la communauté est firebase/php-jwt (v7, exige PHP 8.0 ou plus récent), installé avec Composer :

composer require firebase/php-jwt
use Firebase\JWT\JWT;
$secret = 'correct-horse-battery-staple-long-enough-secret';
$token = JWT::encode([
  'sub' => '1234567890',
  'name' => 'John Doe',
  'exp' => time() + 3600,
], $secret, 'HS256');
var_dump(substr_count($token, '.')); // int(2): en-tête, payload, signature

Note de version pour la ligne v7 de la bibliothèque : les algorithmes HMAC imposent une longueur minimale de clé, donc un secret HS256 de moins de 32 octets est refusé avant même que l'encodage ne commence. Les secrets longs sont la norme de toute façon ; ça fait juste refuser à la bibliothèque de rester légère dessus.

La bibliothèque gère la conversion base64url, la signature et les contrôles d'expiration pour vous, et elle lève des exceptions typées au lieu de rendre des données à mi-confiance. Quand vous écrivez des tokens avec elle, vous ne touchez jamais base64_encode() directement, ce qui est exactement comme ça devrait être.

HTTP : Basic Auth et la poignée de main WebSocket

Deux travaux de construction de headers où PHP fait le Base64 et le protocole fait le reste.

HTTP Basic auth (RFC 7617) : le client envoie Authorization: Basic plus le Base64 de username:password. Le construire, c'est une concaténation de chaînes :

function basic_authorization_header(string $username, string $password): string
{
  return 'Basic ' . base64_encode($username . ':' . $password);
}
$headers[] = 'Authorization: ' . basic_authorization_header('alice', 'secret123');

Dites-le à voix haute une fois, parce que le RFC vous y oblige : c'est un encodage, pas une protection. N'importe qui avec une capture de paquets retrouve les deux moitiés en une touche, donc la Basic auth n'a sa place que sur des connexions HTTPS.

La poignée de main WebSocket (RFC 6455) : le serveur prouve qu'il a entendu le client en renvoyant une clé transformée. Il concatène le Sec-WebSocket-Key du client avec un GUID magique fixe, prend le SHA-1 du résultat, et encode le digest en base64. C'est du Base64 standard, padding compris, parce qu'il vit dans un header, pas dans une URL :

function websocket_accept_key(string $clientKey): string
{
  return base64_encode(hash('sha1', $clientKey . '258EAFA5-E914-47DA-95CA-C5AB0DC85B11', true));
}
$accept = websocket_accept_key('dGhlIHNhbXBsZSBub25jZQ==');
var_dump($accept); // string(28) "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

Cet exemple est celui du RFC lui-même, ce qui en fait un auto-test pratique : si votre implémentation produit les mêmes 28 caractères, la couche WebSocket parle correctement.

E-mail : le cas d'usage d'origine

Tout le reste de cet article est un descendant d'un seul fait : SMTP a été conçu pour porter du ASCII 7 bits, et les gens voulaient envoyer des binaires. La réponse du standard MIME, dans la section 6.8 de RFC 2045, c'était Base64 en tant que Content-Transfer-Encoding, avec les deux règles de la maison que vous avez déjà rencontrées : des lignes de 76 caractères au maximum, et des décodeurs qui ignorent tous les caractères en dehors de l'alphabet. Donc une pièce jointe PDF voyage comme ceci :

$pdf = file_get_contents('/var/www/uploads/report.pdf');
$attachment = chunk_split(base64_encode($pdf), 76, "\r\n");
// la bibliothèque de mail met maintenant $attachment dans la partie MIME,
// avec Content-Transfer-Encoding: base64

Les chiffres pratiques : l'encodage lui-même coûte 33 %, et le CRLF tous les 76 caractères coûte un peu plus, donc une pièce jointe de 100 Ko part comme environ 137 Ko de texte. Quand vous écrivez des e-mails depuis PHP, les bibliothèques (PHPMailer et ses parents stables) font l'habillage pour vous, et vous leur donnez le binaire brut. Si vous voyez un jour un mur de lettres large de 76 caractères dans un fichier .eml brut, vous savez maintenant l'algorithme exact qui l'a produit.

Armure PEM pour clés et certificats

Les clés et les certificats ont besoin de plus qu'un mur de lettres ; ils ont besoin de libellés. L'armure PEM, c'est une ligne BEGIN, un bloc Base64 habillé en 64 caractères, et une ligne END, une convention héritée de Privacy-Enhanced Mail (RFC 1421) et maintenue en vie par OpenSSL. L'extension openssl de PHP produit et consomme cette forme directement :

$res = openssl_pkey_new([
  'private_key_bits' => 2048,
  'private_key_type' => OPENSSL_KEYTYPE_RSA,
]);
openssl_pkey_export($res, $pem);
// $pem est déjà armé : libellé BEGIN, lignes de 64 caractères, libellé END

Le cas intéressant, c'est quand l'armure doit être reconstruite à la main, par exemple quand vous recevez des octets DER bruts d'une API et qu'il vous faut un fichier PEM pour un outil qui ne lit que le PEM. La convention, c'est 64 caractères par ligne, des retours à la ligne LF, et un libellé qui nomme le contenu :

$rearmored = "-----BEGIN CERTIFICATE-----\n"
  . chunk_split(base64_encode($der), 64, "\n")
  . "-----END CERTIFICATE-----\n";
var_dump(openssl_x509_parse($rearmored) !== false); // bool(true) : l'armure est valide

Faites une erreur sur le libellé et le fichier est du n'importe quoi, aussi parfait soit le Base64. Et faites une erreur sur la longueur de ligne et la plupart des outils le liront quand même, parce que les décodeurs ignorent les retours à la ligne, mais les outils de diff et les humains en souffriront. Soixante-quatre, c'est le chiffre.

Fichiers de configuration, variables d'environnement et bases de données

Base64 est un conteneur de texte, ce qui en fait un outil de contrebande pour des valeurs qui casseraient sinon leur conteneur. Un DSN de base de données bourré de points-virgules et de guillemets, un JWT dans un fichier .env, un blob binaire dans une colonne TEXT : tous deviennent une longue chaîne sûre.

La saveur variable d'environnement est un rituel en deux étapes. Une fois, sur la machine qui construit la configuration, vous encodez :

echo 'DB_DSN_B64=' . base64_encode('pg:host=db;password=qu"ote') . PHP_EOL;

Puis, à chaque démarrage de l'application, vous décodez et validez au boot, pour qu'une configuration collée à moitié échoue fort au lieu d'échouer cryptiquement :

$dsn = base64_decode(getenv('DB_DSN_B64') ?: '', true);
if ($dsn === false) {
  exit('DB_DSN_B64 is not valid Base64.');
}

Pour les bases de données, la même idée stocke du binaire dans des colonnes texte. La facture de taille s'applique : la valeur stockée est environ 33 % plus grosse que le blob, donc un fichier de 1 Mo occupe environ 1,33 Mo dans la colonne, et vous devriez choisir le type de colonne en le gardant en tête. Et la même mise en garde que partout ailleurs : c'est une sécurité de format, pas un secret. N'importe qui peut lire la configuration ou interroger la colonne peut l'inverser en un appel. Si la valeur est sensible, chiffrez-la ; Base64 la rend seulement portable.

Grandes données et mémoire stable

L'encodage est la direction qui vous coûte : la sortie est un tiers plus grosse que l'entrée, donc un binaire de 2 Go réclame 2,66 Go de chaîne encodée en mémoire. Dans un processus web long-lived ou sur un hôte à mémoire limitée, c'est une raison de streamer au lieu d'avaler, et PHP vous donne deux façons.

La première, c'est le filtre de flux convert.base64-encode, le jumeau streaming de la fonction. Il prend des paramètres sous forme de tableau associatif : line-length pour la largeur d'habillage et line-break-chars pour le séparateur, ce qui reproduit l'effet de chunk_split() sans tenir toute la chaîne :

$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
stream_filter_append($out, 'convert.base64-encode', STREAM_FILTER_WRITE, [
  'line-length' => 64,
  'line-break-chars' => "\n",
]);
stream_copy_to_stream($in, $out);
fclose($in);
fclose($out);

La seconde, c'est l'astuce classique des 57 octets, et c'est un petit morceau de légende PHP. Une ligne MIME de 76 caractères tient exactement 57 octets de données d'origine, donc si vous lisez le fichier d'entrée par blocs qui sont des multiples de 57 octets, chaque bloc s'encode indépendamment, sans bits restants à porter d'un bloc à l'autre. Lire par blocs de 8151 octets (57 fois 143 : 143 lignes de sortie complètes de 76 caractères, proche du tampon d'E/S traditionnel de 8192 octets de PHP) garde la mémoire à plat pendant que le fichier sort en streaming parfaitement MIME :

$in = fopen('/var/www/uploads/big.bin', 'rb');
$out = fopen('/var/www/uploads/big.b64', 'wb');
while (!feof($in)) {
  $plain = fread($in, 57 * 143);
  $encoded = chunk_split(base64_encode($plain), 76, "\r\n");
  fwrite($out, $encoded);
}
fclose($in);
fclose($out);

Laquelle choisir ? Le filtre quand vous voulez que PHP possède la plomberie et que vous ne vous souciez pas des bordures exactes des blocs ; la boucle des 57 octets quand vous voulez une sortie MIME déterministe, des accroches de progression, ou un plafond dur sur la taille du tampon. Dans les deux cas, l'empreinte mémoire reste d'un bloc, pas d'un fichier.

Pièges à l'accent PHP

Les pièges qui apparaissent dans de vraies bases de code PHP, réunis au même endroit :

  • Double encodage. Le classique : une valeur qui est déjà du Base64 (d'une variable d'environnement, d'une base de données, d'un script précédent) repasse dans base64_encode() parce que personne n'a vérifié. Le résultat se décode une fois et rend... encore du Base64. Le remède, c'est un contrôle d'aller-retour à la frontière, ou une seule fonction connue qui possède tous les encodages de la base de code.
  • Le + dans les URL. La sortie standard contient + et /. Dans une chaîne de requête encodée en formulaire, le plus devient une espace avant que votre code ne le voie ; dans un chemin, c'est un séparateur. Pour tout ce qui est lié aux URL, émettez du base64url ou encodez la valeur entière en pourcentage avec rawurlencode().
  • Le séparateur final. chunk_split() termine son résultat par le séparateur même sur les multiples exacts de la longueur de ligne. Une ligne vide finale est d'habitude inoffensive (les décodeurs l'ignorent) mais elle fait trébucher les compteurs de lignes naïfs et les outils de diff. rtrim() le séparateur si le consommateur est pointilleux.
  • Désaccord d'habillage. Écrire avec des lignes MIME de 76 caractères et avoir un consommateur qui s'attend à des lignes PEM de 64 caractères (ou l'inverse), ce n'est pas un problème de décodage, parce que les décodeurs ignorent les retours, mais c'est un problème d'outils à base de lignes et de lecture humaine. Choisissez la convention que votre destination s'attend et tenez-y vous.
  • Le retour à la ligne final est des données. base64_encode() encode chaque octet, y compris un retour à la ligne à la fin d'un fichier texte. Quand deux systèmes produisent des Base64 « différents » pour du texte qui a l'air pareil, un \n final est le suspect habituel.
  • Base64 n'est pas du chiffrement. Encoder un mot de passe avant qu'il touche la base de données ne le protège pas ; ça le met en forme. La colonne « chiffrée » est à un appel de fonction du texte clair pour n'importe qui a un accès aux requêtes. Chiffrez ou hachez les vrais secrets ; Base64 est un costume de transport.
  • La mémoire est un tiers plus grosse. Sur un build PHP 32 bits ou un hôte à limites mémoire serrées, encoder un gros binaire peut échouer tout court. Streamez, comme montré ci-dessus, avant de retoucher memory_limit.
  • Aucun retour à la ligne n'est ajouté, jamais. « MIME base64 » dans la description de la fonction ne veut pas dire « sortie habillée en MIME ». Si votre sortie a besoin de lignes de 76 caractères, vous les ajoutez avec chunk_split() ou le filtre.

Une brève histoire de base64_encode

Le côté encodage de l'histoire PHP est presque rafraîchissantement ennuyeux, au meilleur sens du terme. base64_encode() est arrivée dans PHP 4 comme fonction du cœur avec un paramètre et aucune option, et elle n'en a gagné aucun depuis. Le mode strict n'a jamais été nécessaire (il n'y a rien de strict à faire quand c'est vous qui produisez les données), l'option de padding n'a jamais été ajoutée, et le travail d'habillage a été délégué à chunk_split() dès le premier jour, c'est pour ça que les deux fonctions sont toujours ensemble dans les listes « See Also » du manuel.

Le manuel porte le chiffre des 33 % depuis aussi longtemps que quiconque s'en souvienne : « les données encodées en Base64 prennent environ 33 % plus de place que les données d'origine ». Cette phrase est encore là aujourd'hui, et c'est pour ça que le chiffre apparaît dans cet article. Le filtre de flux convert.base64-encode a rejoint plus tard, et il a eu son propre bug à grandir : en 2015, PHP a corrigé un défaut (bug #68532) où le filtre, en mode lecture sur des flux en mémoire, pouvait omettre le dernier caractère de padding, ce qui est exactement le genre de corruption silencieuse duquel le chemin basé sur la fonction ne souffre jamais. PHP 8.0 a ajouté les types natifs de paramètre string et de retour, et c'est là que le journal se termine. Une fonction, un paramètre, vingt ans, zéro option : un monument à l'art de faire la surface juste du premier coup.

Faits PHP rigolos

Parce qu'une référence n'est pas complète sans ses bizarreries :

  • L'identité vide. base64_encode('') vaut ''. Pas de padding, pas de sortie, pas de surprise : du vide en entrée, du vide en sortie.
  • Une adresse étrange. Le manuel PHP classe base64_encode() sous « URLs » dans le livre « Autres extensions de base ». Il n'y a pas de chapitre « encodage » ; « URLs », c'est là que vous le trouverez, aux côtés de parse_url().
  • L'alphabet n'a jamais bougé. Les mêmes 64 caractères sortent de l'encodeur de PHP depuis PHP 4. Une chaîne Base64 produite par un script PHP 4 sur une machine Windows en 2001 se décode identiquement sur PHP 8.4 sous Linux aujourd'hui. C'est de l'interopérabilité avec un historique de 25 ans.
  • Un octet et trois octets font la même longueur. Tous les deux produisent quatre caractères ; seul le padding les distingue. C'est pour ça que le tableau de tailles de cet article existe.
  • Cinquante-sept, c'est un nombre magique. Une ligne MIME de 76 caractères tient exactement 57 octets de données d'origine, et c'est cette coïncidence qui rend possible la boucle de blocs du streaming de la section grandes données, sans porter d'état entre les lectures.
  • Il a un cousin de l'ère modulaire. Les « See Also » de base64_encode() listent même convert_uuencode(), l'habillage PHP de uuencode, le format qui encodait les binaires pour l'e-mail avant que MIME ne standardise Base64. Il vit dans le chapitre « fonctions de chaîne », mais c'est le registre fossile du but d'origine de cette fonction.
  • Les vieux navigateurs étaient pointilleux sur le padding. Une note php.net de 2004 rapporte qu'Internet Explorer refusait les noms de cookies contenant =, c'est pour ça que le code vétéran retire parfois les paddings finaux du Base64 stocké dans des cookies. Les installations modernes n'ont pas besoin de cette astuce, mais elle explique les appels bizarres de rtrim($x, '=') que vous pouvez hériter.

L'envers

Voilà pour le côté encodage, et c'est le plus facile des deux : la fonction ne peut pas échouer, la sortie est déterministe, et le format est un que vous contrôlez de bout en bout. La direction plus dure est celle où vous recevez le Base64 des autres : leurs choix de padding, leurs retours à la ligne, leurs dialectes sûrs pour les URL, leurs collages corrompus. C'est là qu'un décodeur a besoin du mode strict, d'un pipeline de validation, et d'une suspicion saine envers tout. Le décodage Base64 en PHP, relié depuis cette page, couvre le côté décodage avec la même profondeur.

Dernière mise à jour : 2026-09-08

Article associé : Décodage Base64 en PHP : un guide complet