Encodage Base64 en C# (CSharp) : un guide complet
Vous avez les octets. Un PNG qui doit voyager dans une réponse JSON, un jeton qui doit tenir dans une URL, une ligne de texte qui va entrer dans un système qui n'accepte que des lettres et des chiffres. Quelque part entre le byte[] que vous avez en main et le canal qu'il doit traverser, C# offre un menu d'encodeurs Base64, et le choix entre eux est la vraie compétence de cette matière. Le classique d'une ligne est dans le framework depuis 2003, les options par span et URL-safe sont arrivées avec les runtimes modernes, et chacune fait des promesses différentes sur la taille, les sauts de ligne et l'alphabet. Cet article parcourt tout le menu, avec des exemples fonctionnels pour chaque vrai travail qu'on demande à un encodeur.
D'abord les règles de la maison, en une haleine, puisque la page d'accueil de ce site explique le format en profondeur : l'encodeur prend chaque groupe de trois octets et écrit quatre caractères d'un alphabet de 64 symboles, en comblant la queue d'un ou deux caractères =, donc vos données repartent environ 33 pour cent plus grasses qu'à l'arrivée. Ce chiffre, et non du code, est le fait le plus important de cet article, et tout ce qui suit est une question de le payer avec bon sens.
Le menu des encodeurs : choisissez votre outil
Voici la famille complète d'API d'encodage dans le monde .NET, avec la situation pour laquelle chacune est faite. Tout est dans le runtime lui-même, à l'exception de la classe URL-safe sur les frameworks plus anciens, qui voyage dans un petit paquet NuGet :
| API | Disponible depuis | À quoi ça sert |
|---|---|---|
Convert.ToBase64String(byte[]) |
.NET Framework 1.1 (2003) | Le classique. Tableau entier en entrée, chaîne avec padding en sortie. Pas d'options, pas de surprises. |
Convert.ToBase64String(byte[], int, int) |
.NET Framework 1.1 (2003) | Encoder une tranche d'un plus grand tableau, sans la copier d'abord. |
Convert.ToBase64String(byte[], Base64FormattingOptions) |
.NET 2.0 (2005) | Le classique avec un régulateur : insérer en option un saut de ligne tous les 76 caractères, à la façon du MIME. |
Convert.ToBase64String(ReadOnlySpan<byte>, Base64FormattingOptions) |
.NET Core 2.1 (2018) | La version span : encoder une vue sur un tampon, sans copie de tableau, sans allocation de tranche. |
Convert.ToBase64CharArray(byte[], int, int, char[], int) |
.NET Framework 1.1 (2003) | Écrire dans un tampon de caractères que vous possédez, et recevoir combien de caractères ont été utilisés. |
Convert.TryToBase64Chars(ReadOnlySpan<byte>, Span<char>, out int, ...) |
.NET Core 2.1 (2018) | Un booléen au lieu d'exceptions : encoder si le tampon suffit, renvoyer false sinon. |
System.Buffers.Text.Base64.EncodeToUtf8, EncodeToUtf8InPlace |
.NET Core 2.1 (2018) | La famille span stricte : des codes d'état au lieu d'exceptions, et une inflation in-place pour les tampons que vous possédez déjà. |
System.Buffers.Text.Base64Url.EncodeToString et ses sœurs |
.NET 9 (2024) | L'alphabet URL-safe, émis sans padding. Sur .NET Framework 4.6.2+ et .NET Standard 2.0 : le paquet NuGet Microsoft.Bcl.Memory. |
ToBase64Transform + CryptoStream |
.NET Framework 1.1 (2003) | Le flux : encoder un fichier pendant qu'il coule, sans jamais tenir tout le payload en mémoire. |
Si votre projet cible une version .NET de 2018 ou plus récente, les sept premières lignes et le duo streaming en bas sont dans la boîte. Base64Url exige .NET 9 ou plus récent, ou le paquet Microsoft.Bcl.Memory sur tout ce qui est plus ancien. Et une note en avant : les bibliothèques .NET 11, en preview au moment d'écrire, avec une version générale attendue à la fin de 2026, ajoutent encore des API de confort et des surcharges Base64 aux types existants, donc le menu continue de grandir. Aucun autre paquet de cet article n'est requis.
L'appel standard : Convert.ToBase64String
Quatre-vingt-dix pour cent de la vie de l'encodage en C# tiennent dans un seul appel. Donnez-lui des octets, et elle vous rend la chaîne qui les porte :
using System;
using System.Text;
string text = "Man";
byte[] bytes = Encoding.UTF8.GetBytes(text);
string packed = Convert.ToBase64String(bytes);
Console.WriteLine(packed);
// TWFu
Remarquez la forme en deux étapes, parce que c'est la question « pourquoi mon Base64 ne correspond pas » la plus courante en C#. Il n'existe pas de surcharge qui prenne une string directement, et c'est par conception : une chaîne C# est du UTF-16, et le framework refuse de deviner quels octets vous vouliez dire quand vous avez dit « encode ce texte ». Vous choisissez d'abord la représentation en octets, avec Encoding.UTF8.GetBytes (ou le charset que les données sont vraiment), et seulement alors l'étape Base64 a lieu. Le reste de la famille classique est le même appel avec une taille plus fine : la surcharge (byte[], int, int) encode une tranche d'un tampon sans copier la tranche, et la surcharge span fait la même chose depuis un ReadOnlySpan<byte>, ce qui est le bon outil quand les données sont une fenêtre sur un plus grand tampon de lecture. Une dernière propriété de l'encodeur classique mérite d'être dite clairement : il ne rate jamais et ne demande jamais rien. Il émet toujours l'alphabet standard, inclut toujours le padding, et vous donne toujours la même chaîne pour la même entrée, donc une chaîne Base64 est une empreinte fiable des octets qui l'ont produite.
La question des 76 caractères : sauts de ligne et Base64FormattingOptions
Il y a un seul régulateur sur l'encodeur classique, et il est là depuis .NET 2.0 : Base64FormattingOptions. Mettez-le sur InsertLineBreaks et l'encodeur insère un saut de ligne après chaque groupe de 76 caractères de sortie, la longueur de ligne que la spécification MIME utilise pour les pièces jointes d'e-mail. Mettez-le sur None, ou utilisez les surcharges sans l'option, et vous obtenez une longue chaîne ininterrompue :
using System;
byte[] bytes = new byte[90];
string plain = Convert.ToBase64String(bytes);
string wrapped = Convert.ToBase64String(bytes,
Base64FormattingOptions.InsertLineBreaks);
Console.WriteLine(plain.Length); // 120
Console.WriteLine(wrapped.Length); // 122, un saut de ligne ajouté après le caractère 76
Deux détails sur ce régulateur comptent en pratique. D'abord, le saut de ligne qu'il insère est la paire Windows, retour chariot plus saut de ligne, et non un simple saut de ligne. Donc la sortie enveloppée contient des séquences \r\n, et n'importe quel code qui « nettoie » plus tard la chaîne en retirant seulement \n se retrouvera avec des retours chariot égarés qui se cachent dans les données. Ensuite, l'enveloppement se fait à 76 caractères de sortie encodée, c'est pour cela que la norme MIME pouvait garantir que le transport d'e-mail, avec ses limites de lignes de 76 ou 78 caractères, ne couperait jamais un groupe de quatre caractères Base64 entre deux lignes : 76 est un multiple de quatre, donc chaque ligne finit sur une frontière de groupe. Vous voulez la forme enveloppée quand vous produisez des corps d'e-mail, des blocs de texte façon PEM, ou n'importe quoi qu'un pipeline de courrier legacy transportera. Vous voulez la forme non enveloppée partout ailleurs : payloads JSON, jetons d'URL, réponses d'API, et fichiers qui seront décodés par un parseur strict qui n'aime pas les surprises. Et vous ne voulez jamais la forme enveloppée dans un JWT, où la spécification interdit explicitement les sauts de ligne, les espaces blancs, et même le padding.
Posséder la sortie : tampons de caractères et API Try
Parfois la chaîne n'est pas l'objectif, c'est le tampon. Vous appendez dans un tableau de caractères de taille fixe, vous écrivez dans un frame de protocole, ou vous ne voulez simplement pas que le runtime alloue la sortie pour vous. Pour ces moments, l'encodeur a un mode tampon de caractères depuis l'époque 1.1, et un mode Try depuis l'ère des span. La méthode tampon de caractères écrit dans un tableau que vous fournissez et vous dit combien de caractères elle a utilisés, donc dimensionner le tampon est votre travail, et la bibliothèque standard vous donne même la formule de dimensionnement :
using System.Buffers.Text;
using System.Text;
byte[] bytes = Encoding.ASCII.GetBytes("Man");
char[] buffer = new char[Base64.GetMaxEncodedToUtf8Length(bytes.Length)];
int written = Convert.ToBase64CharArray(bytes, 0, bytes.Length, buffer, 0);
string packed = new string(buffer, 0, written);
Console.WriteLine(packed);
// TWFu
La sœur Try fait le même travail depuis des span et répond par un booléen. Elle encode l'entrée dans votre span de destination, rapporte le nombre de caractères dans le paramètre de sortie, et renvoie false si la destination était trop petite, sans rien écrire. Cette dernière propriété la rend sûre à utiliser avec des tailles d'entrée non fiables : vous n'obtenez jamais un tampon à moitié rempli d'un appel en échec :
using System;
byte[] bytes = { 1, 2, 3 };
char[] buffer = new char[4];
if (Convert.TryToBase64Chars(bytes, buffer, out int written,
Base64FormattingOptions.None))
{
Console.WriteLine(new string(buffer, 0, written));
// AQID
}
else
{
Console.WriteLine("Buffer too small, nothing was written.");
}
Pour la famille span stricte de System.Buffers.Text.Base64, la même forme existe avec le contrat OperationStatus au lieu d'un booléen : EncodeToUtf8 remplit un span d'octets que vous possédez et vous dit, par le statut, s'il a fini, s'il a épuisé la place, ou s'il a besoin de plus d'entrée, et EncodeToUtf8InPlace est celle que vous prenez quand les données binaires sont déjà dans un tampon que vous êtes prêt à laisser grandir : l'encodage gonfle les données, donc la méthode écrit le texte Base64 par-dessus la fin du même tampon et rapporte combien le résultat fait. Toutes celles-ci partagent une règle sur le dimensionnement : la sortie pour n octets d'entrée fait toujours 4 * ceil(n / 3) caractères padding compris, et les assistants GetMaxEncodedToUtf8Length et Base64Url.GetEncodedLength implémentent cette arithmétique - le second pour la longueur sans padding, qui est toujours de la taille avec padding ou plus courte - donc dimensionnez depuis les assistants, et jamais depuis une constante mémorisée.
L'encodeur URL-safe : Base64Url
L'alphabet standard a deux caractères que les URL n'aiment pas. Le + d'une chaîne de requête est régulièrement décodé en espace par les règles d'analyse de formulaire, et / comme = veulent un encodage en pourcentage avant de pouvoir voyager dans un chemin ou un paramètre. La variante URL-safe du Base64, définie dans la section 5 de la RFC 4648, échange + et / contre - et _, qui n'ont besoin d'aucun échappement nulle part, et rend le padding final de = optionnel. Depuis .NET 9, le runtime a une classe dédiée pour elle, System.Buffers.Text.Base64Url, et elle a un comportement qui surprend à la première rencontre : elle n'émet pas du padding du tout :
using System.Buffers.Text;
byte[] bytes = { 1, 2 };
string classic = Convert.ToBase64String(bytes);
string urlSafe = Base64Url.EncodeToString(bytes);
Console.WriteLine(classic); // AQI=
Console.WriteLine(urlSafe); // AQI
Cette différence est tout l'objet. Un segment JWT, un identifiant de téléversement, un jeton dans une chaîne de requête, une valeur dans un chemin d'URL : tous veulent la forme URL-safe sans padding, et Base64Url.EncodeToString la donne directement, avec l'alphabet et le padding tous deux gérés comme ces formats l'indiquent. La classe a la famille complète, encodage en chaîne, en span de caractères, et en span d'octets UTF-8, plus GetEncodedLength pour dimensionner les tampons et IsValid pour valider l'entrée à l'arrivée. Si votre projet tourne sur un runtime plus ancien, ajoutez le paquet Microsoft.Bcl.Memory, que Microsoft publie pour rétroporter la classe vers .NET Framework 4.6.2 et plus :
dotnet add package Microsoft.Bcl.Memory
Et si vous ne pouvez pas utiliser le paquet, la version faite main est l'encodeur classique plus deux remplacements et un effacement, que vous croiserez dans une grande quantité de bases de code C# :
using System;
using System.Text;
byte[] bytes = Encoding.UTF8.GetBytes("Hello World!");
string packed = Convert.ToBase64String(bytes)
.Replace('+', '-')
.Replace('/', '_')
.TrimEnd('=');
Console.WriteLine(packed);
// SGVsbG8gV29ybGQh, sûr pour les URL et sans padding
L'ordre des opérations dans cette chaîne mérite qu'on s'y attarde : les échanges de caractères se font sur la sortie standard, et le padding est effacé en dernier, parce que l'effacer d'abord ne changerait rien mais rendrait le code plus dur à lire, et échanger après l'effacement fonctionnerait toujours mais c'est ainsi que naissent les bugs subtils. Utilisez cette forme pour les jetons, les identifiants, et n'importe quoi qui vivra dans une URL, et réservez l'alphabet standard pour les corps d'e-mail, les payloads JSON et les fichiers, où +, / et = sont parfaitement chez eux.
Alimenter l'encodeur : chaînes, charsets et le choix d'encodage
Chaque travail d'encodage qui part du texte commence par la même décision discrète : en quels octets ce texte devient-il ? L'étape Base64 est déterministe et innocente, mais l'étape Encoding qui la précède est l'endroit où les sorties divergent, et la divergence peut être silencieuse. L'UTF-8 est l'hypothèse par défaut du web moderne, et c'est le bon défaut ici : il fait l'aller-retour de chaque langue, c'est ce que chaque autre plateforme supposera quand elle décodera votre payload, et c'est ce que Encoding.UTF8 vous donne en un seul appel :
using System;
using System.Text;
string original = "h\u00e9llo \u4e16\u754c";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
string packed = Convert.ToBase64String(utf8);
Console.WriteLine(packed);
// aMOpbGxvIOS4lueVjA==
Regardez maintenant le même caractère encodé via un charset différent, et voyez pourquoi « le même texte » n'est pas une chose bien définie sans un charset attaché :
using System;
using System.Text;
string euro = "\u20ac";
string asUtf8 = Convert.ToBase64String(Encoding.UTF8.GetBytes(euro));
string asLatin1 = Convert.ToBase64String(
Encoding.GetEncoding("ISO-8859-1").GetBytes(euro));
Console.WriteLine(asUtf8); // 4oKs
Console.WriteLine(asLatin1); // Pw==
Deux chaînes Base64 différentes pour le même signe euro, toutes deux parfaitement valides, et seule l'une d'elles se décodera de nouveau en signe euro de l'autre côté. Le piège au rayon de dévastation le plus large est Encoding.Default : sur .NET Framework sous Windows, c'est la page de codes ANSI du système, tandis que sur .NET (Core), c'est de l'UTF-8, donc un programme qui encode avec Encoding.Default produit un Base64 différent sur une machine de 2010 que sur une de 2025, et les deux sorties se décodent « correctement » sur leur plateforme d'origine. Si un payload décodé arrive plein de mojibake accentué, l'encodage d'origine utilisait un charset différent de celui que le décodage supposait, et la correction est de ce côté du tuyau : fixez l'encodage explicitement, dans les deux sens, dans un code qui survivra à l'équipe qui l'a écrit. Et une note finale sur le système de types lui-même : une chaîne C# est du UTF-16, donc si vous passez jamais des unités de code UTF-16 brutes à l'encodeur (en appelant Encoding.Unicode.GetBytes), chaque caractère ASCII coûte deux octets et votre sortie double de taille sans aucun bénéfice, parce que le décodeur de l'autre côté le lira comme du texte UTF-16, et non comme les octets de votre chaîne d'origine. Le Base64 porte les octets que vous lui donnez, et il s'en fiche de ce qu'ils signifient.
Fichiers : du disque à une chaîne
Les fichiers sont le payload d'encodage le plus courant et le plus indulgent, parce qu'il n'y a pas de question de charset : les octets sur le disque sont les données, et l'encodeur s'en fiche qu'elles épèlent un mot ou une forme d'onde. L'aller-retour tient en une lecture, un encodage, et une écriture, et la seule vraie décision est de savoir où va le résultat :
using System.IO;
byte[] bytes = File.ReadAllBytes("photo.png");
string packed = Convert.ToBase64String(bytes);
File.WriteAllText("photo.b64", packed);
Console.WriteLine(packed.Length + " characters for "
+ bytes.Length + " bytes of image.");
Le calcul de taille est toute l'histoire, et il vaut la peine de le faire avant de choisir un transport. Un méga-octet de fichier devient 1 333 336 caractères Base64, et comme une chaîne C# stocke deux octets par caractère, ce résultat encodé occupe environ 2,7 méga-octets en mémoire en tant que chaîne. Un fichier de dix méga-octets devient une chaîne de treize méga-octets posée dans vingt-six méga-octets de mémoire gérée. Rien de tout cela n'est un problème pour une photo ou un blob de configuration, et c'est une très bonne raison d'utiliser l'encodeur en flux, plus bas, quand le payload est une vidéo. Le motif ci-dessus est celui à prendre pour n'importe quoi qui tient confortablement en mémoire, et c'est aussi le motif que chaque fonctionnalité « téléverser un fichier en Base64 dans un corps JSON » utilise discrètement : lire le fichier, l'encoder, mettre la chaîne dans le JSON, et laisser la couche API faire son travail.
Images sur le web : construire des data URIs
Le consommateur le plus visible des images encodées est le web, et le format du web pour « une image qui vit dans le document » est le data URI : un schéma data: suivi du type MIME, d'un drapeau ;base64, d'une virgule, et des octets encodés. En construire un en C# est de la concaténation de chaînes, et c'est l'encodeur qui fait tout le vrai travail :
using System.IO;
byte[] png = File.ReadAllBytes("logo.png");
string packed = Convert.ToBase64String(png);
string dataUri = "data:image/png;base64," + packed;
Console.WriteLine(dataUri.Substring(0, 30));
// data:image/png;base64,iVBORw0K
Le préfixe iVBORw0KGgo dans cette sortie est un point de contrôle utile : c'est la forme Base64 de la signature PNG de huit octets, donc n'importe quel PNG que vous encodez commencera ainsi, et tout data URI PNG qui ne commence pas ainsi n'est pas un PNG. Trois notes pratiques appartiennent à ce motif. D'abord, le data URI est une copie complète de l'image, gonflée d'un tiers, intégrée dans votre HTML ou votre CSS, donc il échange une requête réseau contre du poids de page permanent, une aubaine pour un favicon de 4 Ko et une arnaque pour une image hero de 4 Mo, et l'encodeur ne négociera pas sur les 33 pour cent. Ensuite, si l'image est grande, redimensionnez-la ou recomprimez-la avant de l'encoder, parce que chaque octet de l'original apparaît dans la page. Enfin, soyez prudent avec le SVG fourni par l'utilisateur dans du HTML destiné aux utilisateurs : un SVG peut porter du script, donc l'intégrer - inline, ou via <object>/<embed> - est une surface XSS classique. Un data URI en <img> simple ne l'exécutera pas dans les navigateurs modernes, mais le même balisage réutilisé dans ces contextes, si. PNG, JPEG, GIF et WebP en data URIs sont inertes ; SVG est celui qui ne l'est pas.
Assembler un JWT à la main
Construire un JSON Web Token de zéro est un rite de passage, et c'est un meilleur rite en C# que dans la plupart des langages, parce que les pièces sont courtes. Un JWT est trois segments base64url joints par des points : l'en-tête encodé, le payload encodé, et la signature. Les deux premiers sont des documents JSON en UTF-8, et la signature est calculée sur les deux premiers segments joints par un point. Voici toute l'assemblage, avec une signature remplaçante, parce que l'étape cryptographique appartient à votre clé de signature et non à l'histoire du Base64 :
using System;
using System.Buffers.Text;
using System.Text;
string headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
string payloadJson = "{\"sub\":\"42\",\"name\":\"Ada\"}";
string header = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(headerJson));
string payload = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(payloadJson));
string signature = "c2lnbmF0dXJl"; // remplaçant de la vraie valeur HMAC ou ECDSA
string jwt = header + "." + payload + "." + signature;
Console.WriteLine(jwt);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl
Deux propriétés de Base64Url.EncodeToString font du travail discret dans cet exemple. Elle émet l'alphabet URL-safe, donc ni + ni / ne peut apparaître dans le jeton, et elle omet le padding, donc aucun = n'apparaît non plus, ce qui est exactement ce que la spécification JWS exige, et exactement ce que Convert.ToBase64String ne ferait pas sans aide. Si vous êtes sur un runtime antérieur à .NET 9, le même travail passe par l'encodeur standard plus la chaîne de correction de la section URL-safe : encodez, échangez les deux caractères, effacez le padding. L'ordre des segments compte pour la signature, qui est calculée sur header plus un point plus payload en octets ASCII bruts, donc assemblez d'abord les deux segments et signez leur concaténation exacte, et non une version reformattée du JSON. Et une limite à garder bien nette : pour tout ce qu'un utilisateur peut atteindre, n'assemblez pas de JWT à la main du tout. Le paquet System.IdentityModel.Tokens.Jwt s'occupe de la construction, de la signature, de la validation et de l'expiration pour vous, et sa gestion du base64url est précisément cet alphabet et cette règle de padding. L'assemblage à la main est pour les tests, les démos, et le jour où vous devez comprendre exactement ce que la bibliothèque fait.
En-têtes HTTP : Basic Auth
Le Base64 apparaît en HTTP pur dans le schéma d'authentification Basic, et le côté encodage est l'un des constructeurs d'en-tête les plus courts du protocole : joignez le nom d'utilisateur et le mot de passe par deux-points, encodez le résultat en UTF-8, faites-en du Base64, et préfixez avec le nom du schéma :
using System;
using System.Text;
string user = "ada";
string password = "s3cret";
string credentials = user + ":" + password;
string header = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials));
Console.WriteLine(header);
// Basic YWRhOnMzY3JldA==
Le charset est la partie délicate : la RFC 7617 laisse le charset par défaut du schéma Basic indéfini pour la compatibilité descendante et n'offre qu'un indice UTF-8 consultatif, mais c'est exactement ce que tous les serveurs modernes attendent, donc un nom d'utilisateur accentué devrait passer par Encoding.UTF8, et non par la valeur par défaut de la plateforme, sinon le serveur décodera une autre chaîne d'octets et refusera la connexion. L'étape Base64 est le seul encodage de l'en-tête : n'encodez pas le résultat en pourcentage, n'encodez pas en URL, ne doublez pas le Base64. Chacune de ces étapes supplémentaires « utile » est un bug connu, et celui du double encodage est le plus courant, parce que les identifiants arrivent parfois pré-encodés depuis une couche qui les a déjà passés en Base64, et un second encodage produit un en-tête qui a l'air plausible et qui échoue silencieusement au serveur. Deux mises en garde sur le schéma lui-même, pour qu'elles tombent ici plutôt que dans la section sécurité où elles seraient diluées : l'authentification Basic transmet le mot de passe dans une forme qui est à une commande du lisible, donc elle n'est acceptable que sur TLS, et même là c'est le mauvais outil pour la plupart des travaux d'API, c'est pour cela que les jetons bearer et les JWT ont pris le relais. Le travail de l'encodeur dans tout cela est petit et honnête : transformer les identifiants joints par deux-points en une chaîne sûre pour un en-tête, et rien de plus.
E-mail : MIME et pourquoi ToBase64Transform n'enveloppe pas
L'e-mail est la maison historique du Base64, et c'est toujours l'endroit d'où vient la règle des lignes de 76 caractères : la spécification MIME enveloppe les corps encodés à 76 caractères avec du CRLF entre les lignes, pour qu'aucun saut SMTP n'ait de raison de les re-plier. C# vous donne deux encodeurs pour ce travail, et ils font des promesses différentes, ce qui vaut la peine d'être compris avant d'en choisir un. Le premier est le classique Convert.ToBase64String avec InsertLineBreaks, que vous avez vu dans la section sauts de ligne, et c'est exactement la forme MIME, enveloppée à 76 avec CRLF, prête à coller sous un en-tête Content-Transfer-Encoding: base64. Le second est ToBase64Transform, le cousin en flux, et voici la surprise : il n'insère pas de sauts de ligne. Il n'a pas de mode pour eux, pas d'option, pas de drapeau de constructeur, et sa sortie est un long flux non enveloppé :
using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("photo.png");
using MemoryStream destination = new MemoryStream();
using ToBase64Transform transform = new ToBase64Transform();
using CryptoStream encoder = new CryptoStream(source, transform, CryptoStreamMode.Read);
encoder.CopyTo(destination);
Console.WriteLine(destination.Length + " characters, no line breaks");
Donc la règle pratique est : pour les payloads d'e-mail petits à moyens, lisez les octets et utilisez l'encodeur classique enveloppant, parce que vous obtenez la forme MIME directement. Pour les grosses pièces jointes, passez en flux avec ToBase64Transform pour garder la mémoire plate, et enveloppez le résultat vous-même si le transport a vraiment besoin de lignes de 76 caractères, en coupant la sortie sur les frontières de groupes (tous les 76 caractères, qui est toujours une frontière de groupe, comme l'a expliqué la section sauts de ligne). La transformation fait la bonne chose en restant non enveloppée : elle traite l'entrée par groupes de trois octets, et les sauts de ligne sont une décision de formatage qui appartient à la couche qui connaît le transport, et non à la couche qui convertit les octets en caractères dans un tuyau.
Flux : encoder de gros fichiers sans les lire deux fois
Quand le payload est une vidéo, une sauvegarde, ou n'importe quoi que vous seriez gêné de tenir dans une chaîne, l'encodeur en flux est toute la solution. Le motif est le miroir du flux côté décodage : un CryptoStream sur le fichier source, avec ToBase64Transform en mode lecture, et un CopyTo vers la cible. Le fichier coule en entrée, le Base64 coule en sortie, et la seule mémoire que le processus détient est le tampon que le flux utilise en interne :
using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("video.mp4");
using FileStream target = File.Create("video.b64");
using ToBase64Transform transform = new ToBase64Transform();
using CryptoStream encoder = new CryptoStream(source, transform, CryptoStreamMode.Read);
encoder.CopyTo(target);
Console.WriteLine("Wrote " + target.Length + " characters.");
Deux faits sur ce motif valent la peine d'être gardés. D'abord, la taille de la sortie est entièrement déterminée par la taille de l'entrée, 4 caractères pour 3 octets, donc vous pouvez réserver l'espace de la cible, précalculer la longueur pour un en-tête content-length, ou budgéter une quota disque avant qu'un seul octet ne coule. Ensuite, la transformation s'attend à son entrée par groupes de trois octets, et CryptoStream gère cet alignement pour vous, alimentant la transformation exactement avec ce qu'elle veut pendant que le fichier file. Si vous pilotez jamais la transformation à la main avec TransformBlock, alimentez-la par multiples de trois, et laissez TransformFinalBlock vider la queue, les un ou deux octets restants qui deviennent le groupe partiel final avec son ou ses caractères de padding. Pour la plupart des applications, la forme CopyTo est tout ce que vous écrirez jamais, et c'est la forme qui se comporte bien sous une limite de mémoire, qui est exactement l'endroit où aiment vivre les gros fichiers.
Configuration, variables d'environnement et bases de données
L'autre travail d'encodage courant dans les applications C# est le travail de stockage : prendre un secret ou un blob binaire et le mettre dans un endroit qui n'accepte que du texte. Les variables d'environnement sont l'exemple visible, parce qu'une variable d'environnement est, par définition, une chaîne :
using System;
using System.Text;
string secret = "p@ssw0rd+and/symbols";
string packed = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));
Environment.SetEnvironmentVariable("SECRET_B64", packed);
string back = Encoding.UTF8.GetString(
Convert.FromBase64String(Environment.GetEnvironmentVariable("SECRET_B64")));
Console.WriteLine(back == secret);
// True
Dans les bases de données, la même idée apparaît le plus souvent comme une propriété byte[] qu'une colonne texte doit contenir, et Entity Framework Core a un mécanisme intégré pour exactement cela, un convertisseur de valeur qui exécute vos fonctions d'encodage et de décodage à chaque lecture et écriture :
using Microsoft.EntityFrameworkCore;
modelBuilder.Entity<Avatar>()
.Property(a => a.ImageData)
.HasConversion(
v => Convert.ToBase64String(v),
v => Convert.FromBase64String(v));
Ce convertisseur est toute l'intégration base de données : le code C# voit un byte[], la colonne voit une chaîne Base64, et l'aller-retour est invisible au point d'appel. Deux mises en garde appartiennent à cette section. D'abord, la colonne paie l'impôt des 33 pour cent : une colonne texte dimensionnée pour la longueur encodée contient un tiers de données de moins que la même largeur en binaire, donc si vous avez une colonne à largeur fixe, dimensionnez-la pour la longueur Base64, et si vous avez un varchar(max) ou équivalent, l'impôt n'est qu'une question de facturation. Ensuite, et c'est celle qui revient sans cesse, le Base64 dans un fichier de configuration est une forme, et non un bouclier. Il garde la valeur sur une seule ligne, la garde hors de la vue des éditeurs de texte, et est à une commande du lisible par quiconque peut lire le fichier. Les secrets ont besoin d'une vraie protection, un coffre-fort de secrets, un coffre de clés, à tout le moins des permissions de fichier, et le Base64 n'est que le format de transport que le secret porte pendant qu'il est posé dans la configuration.
Depuis la ligne de commande
Chaque encodeur mérite une vie console de 15 lignes, et celui de C# est agréable parce que la sortie est une chaîne simple pour laquelle la sortie standard a été faite. Voici l'outil entier : il prend un chemin de fichier ou l'entrée standard, l'encode, et écrit le Base64 dans le terminal où n'importe quel pipeline du shell peut le prendre :
using System;
using System.IO;
using System.Text;
string input = args.Length > 0
? File.ReadAllText(args[0])
: Console.In.ReadToEnd();
byte[] bytes = Encoding.UTF8.GetBytes(input);
Console.WriteLine(Convert.ToBase64String(bytes));
Construisez-le une fois et il se tient à côté de l'utilitaire base64 du shell lui-même pour les jours où vous voulez spécifiquement le comportement de l'encodeur .NET : le même alphabet, le même padding, et le traitement UTF-8 du runtime C# de n'importe quoi que le tuyau lui remet. Pour les fichiers binaires, le même squelette avec File.ReadAllBytes au lieu de File.ReadAllText est tout le changement, et la sortie décrit alors les octets exacts du fichier plutôt que son interprétation texte. L'outil est aussi une bonne sonde : faites passer un fichier dedans, refaites passer la sortie dans le décodeur de l'article décodage, et comparez les deux fichiers avec diff, ce qui est une vérification bout-en-bout satisfaisante que les deux côtés du tuyau s'accordent sur chaque octet.
Le padding, ou les signes égaux de la queue
Les derniers caractères = d'une chaîne Base64 sont la comptabilité du format, et les encodeurs de C# ne sont pas d'accord entre eux sur eux, ce qui est la source d'un bug d'interopérabilité précis et courant. Le classique Convert.ToBase64String met toujours du padding, parce que le décodeur classique avec qui il est apparié s'y attend toujours. Base64Url.EncodeToString ne met jamais de padding, parce que les consommateurs URL-safe qu'il vise, les JWT et les API de jetons, s'attendent toujours à la forme compacte. Quand votre sortie traverse vers un monde à l'attente opposée, la correction est arithmétique, et c'est la même arithmétique que l'article décodage a montrée pour la direction inverse :
using System;
string padded = Convert.ToBase64String(new byte[] { 1, 2 });
Console.WriteLine(padded); // AQI=
Console.WriteLine(padded.TrimEnd('=')); // AQI, ce qu'un consommateur URL-safe veut
string compact = "AQI";
string restored = compact + new string('=', (4 - compact.Length % 4) % 4);
Console.WriteLine(restored); // AQI=, ce qu'un décodeur classique veut
La formule (4 - length % 4) % 4 est tout l'univers du padding : elle ajoute zéro, un ou deux caractères pour amener la longueur sur un multiple de quatre, et le modulo externe empêche une entrée déjà pourvue de padding d'en gagner. Deux avertissements sur le padding, parce que c'est là que le code bien intentionné se trompe. Ne traitez jamais le = comme des données : il ne porte aucune information, donc encoder une chaîne qui contient déjà du padding comme si c'était du payload, ou encoder en URL le = en %3D dans une chaîne de requête, sont tous deux des façons de produire une sortie qui a l'air juste et se décode faux. Et méfiez-vous de la petite famille de payloads legacy où le padding était écrit avec un caractère différent, un point dans certains anciens systèmes, au lieu du = standard : si une valeur que vous recevez utilise un point là où vous attendez du padding, normalisez-la de nouveau en = avant de décoder, ou passez-la par le chemin URL-safe sans padding.
À quelle vitesse ça tourne
L'encodage Base64 dans .NET moderne est rapide, et la partie intéressante est l'histoire de la mémoire, et non l'histoire du processeur. Les implémentations du runtime sont optimisées avec des instructions vectorielles SIMD quand le matériel les supporte, et des entrées de plusieurs méga-octets s'encodent en quelques millisecondes à une quinzaine de millisecondes sur une machine de bureau ordinaire, assez rapide pour que l'encodeur soit effectivement gratuit dans n'importe quelle application que vous écrivez. Le conseil de performance qui change vraiment le code a à voir avec la forme. La sortie est une chaîne C#, et une chaîne C# stocke deux octets par caractère, donc le coût en mémoire d'un résultat encodé est d'environ 2,7 octets par octet d'entrée (4 caractères pour 3 octets d'entrée, à 2 octets par caractère), ce qui est un chiffre à connaître quand le payload est dans les méga-octets. Si vous encodez des milliers de petits payloads dans une boucle, préférez les API span et tampon de caractères, qui écrivent dans des tampons que vous réutilisez, aux API chaîne, qui allouent une chaîne gérée neuve par appel. Si vous encodez un gros fichier, sautez la chaîne entièrement et utilisez la transformation en flux, parce que le coût de 2 octets par caractère de tenir une chaîne de 13 méga-octets est du gaspillage pur quand un CopyTo aurait gardé le jeu de travail dans les tampons du flux. Et si vous produisez une sortie enveloppée en MIME, souvenez-vous que le passage d'enveloppement est un second parcours des données, donc enveloppez seulement quand le transport en a besoin, et non par défaut.
La conversation sécurité
Le côté encodeur du Base64 a une leçon de sécurité, et c'est l'inverse de celle du décodeur : c'est vous qui faites le choix d'exposer des données lisibles, et le format ne vous arrêtera pas. Le Base64 est un encodage, pas un chiffrement. Il n'a pas de clé, pas d'algorithme, et pas de secret d'aucune sorte, et la sortie de votre appel ToBase64String est à une commande de l'entrée, sur n'importe quelle machine, dans n'importe quel langage, par n'importe qui. Donc la première règle porte sur ce que vous choisissez d'encoder : n'envoyez jamais un mot de passe, un jeton, ou un secret dans un fichier de configuration « protégé » par Base64, parce que la protection ne va pas plus loin qu'un appel de décodage, et la personne qui lit la configuration a la commande. Si la valeur doit être secrète, elle a besoin d'une vraie protection, et le Base64 n'est que la forme qu'elle porte pendant qu'elle est posée dans le champ texte.
La deuxième leçon porte sur le canal, et elle est spécifique aux choses que cet article construit. Un en-tête d'authentification Basic porte le mot de passe dans une forme que n'importe quel proxy, n'importe quel journal, et n'importe quelle middlebox peut lire, c'est pour cela que le schéma n'est acceptable que sur TLS et est surtout obsolète en dehors des intégrations legacy. Un data URI dans le HTML porte l'image, et si l'image est un SVG fourni par l'utilisateur, il porte tout ce que le SVG porte, c'est pour cela que le cas SVG-dans-data-URI a besoin de la même attention que tout contenu d'utilisateur. Et une valeur Base64 dans une URL est, littéralement, dans l'URL, ce qui veut dire qu'elle est dans l'historique du navigateur, le journal d'accès du serveur, l'en-tête referrer, et le cache du proxy, donc les jetons qui doivent rester privés n'appartiennent pas aux chaînes de requête, avec padding ou sans. L'encodeur fait son travail honnête dans les trois cas, transformant des octets en une chaîne sûre à porter. La sécurité est dans ce que vous portez, et où, et le format est un meilleur messager que la plupart, mais c'est un messager, et non un coffre-fort.
Les pièges dans lesquels tombent les encodeurs C#
Ce sont les pièges qui reviennent sans cesse du côté encodage du code C#, et chacun a une cause concrète dans la façon dont le framework fonctionne :
- Le charset que vous n'avez pas choisi. Encoder une chaîne avec
Encoding.Defaultproduit un Base64 différent sur .NET Framework (la page de codes ANSI de Windows) et sur .NET (UTF-8). Les sorties sont toutes deux valides, toutes deux se décodent « correctement » sur leur plateforme d'origine, et ce ne sont pas les mêmes octets. Fixez l'encodage explicitement. - L'encodage double. L'entrée était déjà du Base64 (une configuration qui a encodé une valeur déjà encodée, une API qui ré-encode son entrée), et l'encodeur, faisant exactement ce qu'on lui a dit, a produit du Base64-de-Base64. Le résultat a l'air plausible, et il se décode couche par couche, c'est ainsi qu'un bug qui prend deux décodages à corriger se découvre en production.
- Des sauts de ligne au mauvais endroit. La forme enveloppée en MIME, avec ses paires CRLF, atterrit dans une chaîne JSON, un segment JWT, ou un paramètre d'URL, où le consommateur strict s'étouffe sur les espaces blancs qu'on ne lui a jamais dit d'attendre. Enveloppez pour le courrier, laissez-le seul partout ailleurs, et si vous retirez l'enveloppement de quelqu'un d'autre, retirez le
\rainsi que le\n. - L'alphabet standard dans une URL. Un
+dans une chaîne de requête est décodé en espace par les règles d'analyse de formulaire, donc une valeur Base64 standard mise dans une URL revient avec des lettres là où étaient les signes plus. Utilisez l'alphabet URL-safe, ou encodez en pourcentage toute la valeur, et jamais les deux. - Le décalage de padding. Votre sortie est avec padding, le consommateur veut compact, ou l'inverse, et aucun des deux côtés n'a tort - ils ne font que ne pas être d'accord. La correction est l'arithmétique de la section padding, appliquée du côté qui connaît l'attente du consommateur, ce qui est habituellement le côté qui écrit le jeton.
- La mémoire qui n'a pas été budgétée. La chaîne encodée fait deux octets par caractère en mémoire, donc un fichier de 10 Mo devient une chaîne de 13 millions de caractères qui pèse environ 27 Mo en mémoire gérée, et une boucle qui construit de telles chaînes une à une apparaîtra dans le profiler comme un remue-ménage d'allocations sans cause visible. Dimensionnez les tampons avec les assistants de longueur, passez en flux les gros, réutilisez les tampons dans les boucles critiques.
- La transformation qui n'enveloppe pas.
ToBase64Transformémet une seule longue ligne. Le code qui fait passer en flux une pièce jointe « prête en MIME » dedans puis l'envoie par e-mail produit une ligne de 120 000 caractères que certains transports re-plieront au milieu d'un groupe, ce qui est exactement la corruption que la règle des 76 caractères a été conçue pour prévenir. - Encoder l'encodage. Remettre une chaîne Base64 à l'encodeur parce que « les données sont déjà du texte » produit une seconde couche. L'encodeur ne sait pas, et s'en fiche, que son entrée a l'air du Base64 ; il encode autant de caractères que la chaîne en a, et le décodeur de l'autre côté reçoit une chaîne Base64 là où il attendait vos données.
Comment l'encodeur a grandi : une visite par version
Le côté encodage de l'API a sa propre frise chronologique, et elle va de la deuxième sortie .NET à celle qui est actuellement en preview :
- .NET Framework 1.1, avril 2003.
Convert.ToBase64StringetToBase64CharArrayarrivent, toute la famille classique en une seule sortie, avec déjà les surcharges de tranche incluses, ce qui est un petit miracle de prescience pour une API de 2003. - .NET 2.0, 2005.
Base64FormattingOptionset la valeurInsertLineBreaksrejoignent la famille, apportant l'enveloppement de lignes du MIME dans le framework et mettant fin à une ère de bouclesSubstringfaites main dans le code d'e-mail. - .NET Core 2.1, 2018. L'ère des span.
Convertgagne l'encodage par span etTryToBase64Chars, et la nouvelle classeSystem.Buffers.Text.Base64arrive avec son contratOperationStatuset l'inflation in-place, construite pour le monde zéro-allocation. - .NET 5, 2020. Les sœurs hexadécimales (
Convert.ToHexStringet compagnie) sortent, le même motif de design appliqué à un alphabet de 16 symboles, et le motif de la classe de conversion devient un style de maison. - .NET 7, 2022.
X509Certificate2.ExportCertificatePemfait que le framework produit le PEM pour vous, marqueurs d'armure, enveloppement à 64 caractères et corps Base64 compris, ce qui prend discrètement à la retraite une classe de code de mise en forme de certificats fait main. - .NET 9, novembre 2024.
System.Buffers.Text.Base64Urlatterrit dans la boîte après des années de demandes de la communauté, avec le paquetMicrosoft.Bcl.Memoryqui le rétroporte vers .NET Framework 4.6.2 et plus, et le comportement sans padding que le code JWT fabriquait à la main depuis toujours. - .NET 11, en preview au moment d'écrire. La prochaine version, attendue à la fin de 2026, ajoute encore des API de confort et des surcharges Base64 aux types existants, continuant la marche vers une surface plus ergonomique.
Le format lui-même a une biographie plus vieille, et c'est la raison pour laquelle l'API C# a l'air de ce qu'elle a. La première utilisation standardisée de ce qu'on appelle aujourd'hui le MIME Base64 était le protocole Privacy-Enhanced Mail en 1987 (RFC 989), la spécification MIME a fixé la forme enveloppée en lignes de 76 caractères en 1993, et la RFC 4648 en 2006 a donné au format sa spécification moderne, consciente des alphabets, y compris la variante URL-safe pour laquelle C# n'a eu un encodeur de premier ordre qu'en 2024. Trois décennies de conventions d'e-mail et de web sont la raison pour laquelle les sauts de ligne, le padding, et les deux alphabets existent tous, et l'encodeur C# est l'endroit où les trois se rencontrent.
Petites merveilles
- Le minimum de quatre caractères. La sortie Base64 non vide la plus petite possible fait quatre caractères, parce que le format pense en groupes de quatre même quand vous ne lui donnez qu'un octet. Un octet de n'importe quoi s'encode en deux lettres et deux signes
=, et cette forme, deux caractères de données portant un chapeau de padding, est une empreinte que vous commencerez à reconnaître dans les configurations et les jetons. - Les NUL sont les bienvenus. L'encodeur n'a pas d'avis sur ce que les octets signifient, donc un tampon plein de zéros s'encode gaiement en un mur de caractères
A, et un fichier binaire avec ses octets NUL intacts fait l'aller-retour sans en perdre un seul. L'anxiété « les chaînes ne peuvent pas porter du binaire » appartient au côté chaîne du système de types, et non à l'encodeur, qui ne voit jamais une chaîne. - Le déterminisme comme fonctionnalité. Les mêmes octets, les mêmes options, toujours la même chaîne. Pas de horodatage, pas de sel aléatoire, pas de variation, c'est pour cela qu'une chaîne Base64 fait une empreinte expéditive mais correcte du contenu d'un fichier : deux fichiers avec le même Base64 sont le même fichier, et le contrôle est une comparaison de chaînes.
- Deux octets par caractère, gratuit. Une chaîne C# est du UTF-16, donc chaque caractère de votre sortie Base64 occupe deux octets en mémoire gérée. L'encodeur ne l'annonce pas, la propriété de longueur ne le rapporte pas, et une chaîne de 13 millions de caractères pèse tout simplement 26 Mo, ce qui est le chiffre à avoir en tête quand le payload est grand.
- Le CRLF par héritage. L'enveloppement du MIME insère des paires retour chariot-saut de ligne même quand votre code tourne sous Linux, parce que la règle vient de la spécification d'e-mail, et non de la plateforme. L'encodeur est historien autant que convertisseur, et il préserve les fins de ligne de 1993 sur une machine de 2026.
- Une surcharge de tranche dès le premier jour.
ToBase64String(byte[], int, int)encode une fenêtre sur un plus grand tableau depuis 2003, quinze ans avant que les span ne rendent l'idée à la mode. Les concepteurs d'API de l'ère 1.1 ont regardé de vrais tampons et ont ajouté la forme décalage-et-longueur, et c'est toujours le bon choix quand les données sont une section d'une lecture plus grande. - La ligne de certificat de 64 caractères. Le PEM enveloppe à 64 caractères, et non 76, et
ExportCertificatePemle sait et enveloppe en conséquence, ce qui est l'un de ces détails discrets qui font de « laissez le framework faire » le bon conseil pour le travail de certificats. Deux largeurs d'enveloppement, une famille de formats, et le framework les garde bien distincts. - Deux alphabets, deux noms. Les 64 valeurs s'appellent « standard » dans une partie de l'API et « URL-safe » dans une autre, et elles diffèrent d'exactement deux caractères : les emplacements 62 et 63.
+et/d'un côté,-et_de l'autre, et chaque bug d'interopérabilité de cet article vit dans le moment où quelqu'un a supposé que les deux côtés étaient les mêmes.
Faire le tour complet
Voilà le côté encodeur, et c'est là que vous prenez les décisions : l'alphabet, le padding, les sauts de ligne, le charset, le tampon. L'autre direction, recevoir du Base64 des autres, avec leurs choix de padding, leurs sauts de ligne, leurs alphabets et leurs jetons, est là que vit l'essentiel de la douleur, parce qu'on ne négocie pas avec un payload. Le décodage Base64 en C#, du classique d'une ligne aux familles span et URL-safe, est traité en profondeur dans l'article compagnon lié ci-dessous, et entre les deux, tout le sujet tient dans votre mémoire de travail, ce qui est le point d'un format aussi vieux et aussi petit.
Dernière mise à jour : 2026-09-08
Article associé : Décodage Base64 en C# (CSharp) : un guide complet