Décodage Base64 en C# (CSharp) : un guide complet
Vous le reconnaissez en un instant : un fleuve de lettres et de chiffres, un + ou un / de-ci de-là, et peut-être un = ou deux qui pendent à la fin. Quelque part entre une réponse d'API, une pièce jointe d'e-mail, un fichier de configuration et un JWT, quelqu'un a glissé des données binaires dans du texte, et maintenant c'est à vous de l'ouvrir. Voici le côté décodage du Base64 en C#, et la première bonne nouvelle, c'est qu'il ne vous faut rien d'autre que le framework. Le décodeur vit dans l'espace de noms System depuis plus de vingt ans, et chaque runtime .NET moderne le livre encore, avec plus d'options et de meilleures performances que l'original.
Un petit rappel, parce que la page d'accueil de ce site explique le format en détail : quatre caractères d'un alphabet de 64 symboles portent trois octets de données, et un ou deux = à la queue marquent les octets restants. Le décodage refait cet échange à l'envers, donc le résultat fait à peu près trois quarts de la taille de l'entrée. Le contour du problème en tête, ouvrons quelques paquets.
La famille des décodeurs : connaissez vos options
Avant le premier exemple, voici toute la famille d'API de décodage à votre portée, et la situation pour laquelle chacune est faite. Tout ce qui figure ici fait partie du runtime .NET 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.FromBase64String(string) |
.NET Framework 1.1 (2003) | Le classique. Une chaîne en entrée, un byte[] neuf en sortie. Ignore les espaces blancs ordinaires, lève une exception pour tout le reste. |
Convert.FromBase64CharArray(char[], int, int) |
.NET Framework 1.1 (2003) | Le même décodage, en lisant depuis une tranche d'un tampon de caractères que vous possédez déjà. |
Convert.TryFromBase64String, Convert.TryFromBase64Chars |
.NET Core 2.1 (2018) | Un booléen au lieu d'exceptions, écriture dans un span que vous fournissez. La garde amicale pour les entrées non fiables. |
System.Buffers.Text.Base64 |
.NET Core 2.1 (2018) | L'API span stricte : des codes d'état au lieu d'exceptions, du décodage in-place, et des pré-vérifications IsValid. |
System.Buffers.Text.Base64Url |
.NET 9 (2024) | L'alphabet URL-safe (- et _ au lieu de + et /), avec ou sans padding. Sur .NET Framework 4.6.2+ et .NET Standard 2.0 : le paquet NuGet Microsoft.Bcl.Memory. |
FromBase64Transform + CryptoStream |
.NET Framework 1.1 (2003) | Le décodage en flux : de fichier à fichier, du réseau au disque, bloc par bloc, sans charger tout le payload. |
Si votre projet cible une version .NET de 2018 ou plus récente, les quatre premières lignes 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 la famille continue de grandir. Rien d'autre dans cet article ne requiert de paquet.
La bête de somme : Convert.FromBase64String
Quatre-vingt-dix pour cent de la vie du décodage en C# tiennent dans un seul appel. Donnez-lui une chaîne, et elle vous rend les octets exacts qui y étaient emballés :
using System;
using System.Text;
string packed = "TWFu";
byte[] bytes = Convert.FromBase64String(packed);
string text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
// Man
Trois détails valent la peine d'être mémorisés. D'abord, la valeur retournée est des octets, pas du texte : c'est un byte[], le décodeur est orienté octets de bout en bout, et c'est exactement ce qu'on veut, parce que le payload peut être une phrase, un PNG, un certificat ou un hash, et qu'aucun d'eux ne doit être traité comme un cas spécial. Le saut des octets vers un texte lisible est une étape distincte et délibérée, qui passe par Encoding, et c'est là que vivent les décisions de charset (on y reviendra plus bas). Ensuite, le décodeur alloue un tableau neuf à chaque appel, dimensionné à la longueur décodée, donc il ne vous remet jamais un tampon avec de la capacité en plus. Enfin, le contrat est petit et honnête : une chaîne vide se décode en un tableau vide, une référence null lève ArgumentNullException, et tout ce qui n'est pas du Base64 valide lève FormatException. Tout le reste est une élaboration de ces trois règles.
Ce qu'il pardonne et ce qu'il refuse
C'est ici que le décodeur C# a une personnalité, et pas des moindres. Il est généreux sur exactement une chose - les espaces blancs - et impitoyable sur tout le reste. Le décodeur saute exactement quatre caractères, où qu'ils apparaissent dans la chaîne : l'espace (U+0020), la tabulation (U+0009), le saut de ligne (U+000A) et le retour chariot (U+000D). Cette politique est une référence assumée à l'e-mail, où les payloads Base64 arrivent enveloppés en lignes de 76 caractères, et cela veut dire qu'une pièce jointe enveloppée en MIME se décode sans prétraitement. Tout ce qui est hors de l'alphabet de 64 symboles, tout ce qui viole les règles de longueur, ou tout ce qui a du padding au mauvais endroit mérite une exception. Regardez le même décodeur en action sur quelques entrées différentes :
| Entrée | Résultat |
|---|---|
"TWFu" |
Se décode en Man (3 octets). |
"TWF\nu" (un saut de ligne au milieu) |
Se décode en Man. Les espaces blancs sont invisibles pour le décodeur. |
"TWFu\u00A0" (un espace insécable à la fin) |
FormatException. Seuls les quatre espaces blancs ci-dessus sont sautés ; le NBSP n'en fait pas partie. |
"TWE" (longueur 3, pas un multiple de 4) |
FormatException. La longueur du payload, espaces blancs ignorés, doit être un multiple de 4. |
"TWFu=" (padding en plus après les données) |
FormatException. Deux caractères de padding au maximum, et uniquement tout à la fin. |
"-_88" (alphabet URL-safe) |
FormatException. Le décodeur standard ne connaît que les 64 caractères de l'alphabet standard. |
null |
ArgumentNullException : La valeur ne peut pas être null. (Paramètre 's') |
Un dernier caprice à mémoriser : chaque délit de format reçoit le même unique message d'erreur, La chaîne d'entrée n'est pas une chaîne Base-64 valide, car elle contient un caractère non Base-64, plus de deux caractères de padding, ou un caractère illégal parmi les caractères de padding. Le message énumère les trois causes possibles et ne dit pas laquelle vous avez touchée, et il ne dit pas où non plus. Si vous déboguez un payload qui échoue, comptez les caractères, vérifiez l'alphabet, et vérifiez le padding, dans cet ordre.
Le décodage sans exceptions : les API Try
Un flux de contrôle piloté par les exceptions est un motif légitime, mais pour une entrée à fort volume ou non fiable, la famille Try est la bonne citoyenne. Elle a été ajoutée dans .NET Core 2.1 et existe en deux variétés : une qui lit depuis une chaîne et une qui lit depuis un span de caractères. Les deux écrivent dans un tampon que vous fournissez et rapportent combien ils l'ont rempli :
using System;
using System.Text;
string payload = "TWFu"; // n'importe quel payload, valide ou non
Span<byte> buffer = stackalloc byte[4096];
if (Convert.TryFromBase64String(payload, buffer, out int written))
{
string text = Encoding.UTF8.GetString(buffer[..written]);
Console.WriteLine(text);
}
else
{
Console.WriteLine("Not a valid Base64 payload.");
}
Deux comportements donnent aux variantes Try l'air d'une espèce différente. Une entrée invalide renvoie false au lieu de lever une exception, donc un flot de payloads malformés vous coûte une branche et pas une exception. Une réserve : une entrée null ne fait pas partie du contrat - elle lève ArgumentNullException - donc la garde Try couvre les payloads cassés, et une valeur qui peut manquer a toujours besoin de sa propre vérification null en premier. La méthode sœur Convert.TryFromBase64Chars fait le même travail depuis un ReadOnlySpan<char>, ce qui est pratique quand le payload vit dans un plus grand tampon de caractères et que vous ne voulez pas découper d'abord une sous-chaîne. Dimensionnez le tampon de sortie avec générosité : la longueur décodée vaut au plus trois quarts de la longueur (hors espaces blancs) de l'entrée, et le paramètre de sortie written vous dit exactement combien est sorti.
Le décodage par span avec System.Buffers.Text.Base64
Quand vous comptez les allocations, ou quand vous voulez que le décodeur décrive ses échecs au lieu de les lever, la classe System.Buffers.Text.Base64 est l'outil. Elle est une classe statique de la bibliothèque standard depuis .NET Core 2.1, et elle travaille sur des spans plutôt que sur des tableaux gérés. Sa méthode de décodage renvoie une valeur OperationStatus avec quatre humeurs : Done (succès), DestinationTooSmall (votre tampon était trop petit), NeedMoreData (l'entrée n'est pas encore un multiple de 4, continuez de lire) et InvalidData (ce n'est pas du Base64). Le dernier paramètre booléen, isFinalBlock, est ce qui distingue ces deux-là : il dit au décodeur si de l'entrée va encore arriver. Voici la forme one-shot, dimensionnée avec le propre assistant de la classe :
using System.Buffers;
using System.Buffers.Text;
using System.Text;
string payload = "TWFu";
byte[] input = Encoding.ASCII.GetBytes(payload);
byte[] output = new byte[Base64.GetMaxDecodedFromUtf8Length(input.Length)];
OperationStatus status = Base64.DecodeFromUtf8(input, output,
out int consumed, out int written, isFinalBlock: true);
if (status == OperationStatus.Done)
{
Console.WriteLine(Encoding.UTF8.GetString(output.AsSpan(0, written)));
// Man
}
Deux autres membres de cette classe méritent un paragraphe. Le premier est IsValid, qui valide un payload sans le décoder. Il existe en version span d'octets et en version span de caractères, et une surcharge rapporte la longueur décodée en même temps que le verdict, ce qui vous permet de dimensionner un tampon avec une seule vérification :
using System.Buffers.Text;
string payload = "TWFu";
if (Base64.IsValid(payload, out int decodedLength))
{
Console.WriteLine("Valid, decodes to " + decodedLength + " bytes.");
// Valid, decodes to 3 bytes.
}
else
{
Console.WriteLine("Rejecting payload before allocating anything.");
}
Le second est DecodeFromUtf8InPlace, pour la situation où le texte Base64 est déjà dans un tampon que vous possédez et où vous n'avez rien contre l'écraser. Le décodage rétrécit les données, donc le résultat est écrit au début du même tampon et la méthode rapporte sa longueur :
using System.Buffers;
using System.Buffers.Text;
using System.Text;
byte[] data = Encoding.ASCII.GetBytes("TWFu");
OperationStatus status = Base64.DecodeFromUtf8InPlace(data, out int written);
if (status == OperationStatus.Done)
{
Console.WriteLine(Encoding.ASCII.GetString(data, 0, written));
// Man, qui vit maintenant dans les trois premiers octets du même tampon
}
Un comportement à garder en poche : cette classe saute aussi les quatre espaces blancs ordinaires (espace, tabulation, saut de ligne, retour chariot), donc un payload enveloppé par lignes se décode aussi bien. Elle est stricte là où ça compte : un payload dont la longueur hors espaces blancs n'est pas un multiple de quatre est InvalidData quand c'est le bloc final, et les caractères hors de l'alphabet standard sont rejetés sur le champ. Aucun nettoyage silencieux nulle part dans cette classe.
Le Base64 URL-safe : la classe Base64Url
Un second alphabet existe pour les mêmes 64 valeurs, et vous le croiserez constamment dans le travail web C#. Dans l'alphabet standard, les valeurs 62 et 63 sont + et /, deux caractères qui causent des ennuis dans les URL : un + dans une chaîne de requête est régulièrement décodé en espace, et / et = ont chacun besoin d'un encodage en pourcentage. La section 5 de la RFC 4648 corrige cela en remplaçant par - et _, qui n'ont aucune signification spéciale dans aucun contexte d'URL, et elle rend le padding final de = optionnel. Le résultat s'appelle base64url, et c'est l'alphabet des JWT, des jetons d'API, des identifiants de téléversement de fichiers et d'une grande quantité d'URL (les identifiants vidéo de 11 caractères de YouTube sont du base64url sans padding).
Depuis .NET 9, la bibliothèque standard livre une classe dédiée pour lui : System.Buffers.Text.Base64Url. C'est la jumelle URL-safe de la classe Base64, avec ses propres assistants de décodage, de validation et de longueur :
using System.Buffers.Text;
using System.Text;
string token = "-__8";
byte[] bytes = Base64Url.DecodeFromChars(token);
Console.WriteLine(BitConverter.ToString(bytes));
// FB-FF-FC
Remarquez ce que l'API classique n'aurait pas fait avec cet exemple. Les mêmes trois octets s'encodent en +//8 dans l'alphabet standard, et Convert.FromBase64String("+//8") fonctionne, mais Convert.FromBase64String("-__8") lève une exception, parce que les caractères URL-safe sont hors de son alphabet. Et les payloads base64url arrivent souvent sans padding, ce que le décodeur classique rejette aussi, parce qu'il tient au groupe de quatre complet. La classe Base64Url gère nativement les deux variantes du problème : elle décode TWE (trois caractères, sans padding) en deux octets Ma, et elle décode TWE= tout aussi bien.
Si votre projet tourne sur un runtime plus ancien, il y a deux chemins pratiques. Sur .NET Framework 4.6.2 et plus, ajoutez le paquet NuGet Microsoft.Bcl.Memory, que Microsoft publie précisément pour rétroporter Base64Url (avec quelques autres types modernes) :
dotnet add package Microsoft.Bcl.Memory
Ou, sans aucun paquet du tout, normalisez le payload avant de le remettre au décodeur classique : replacez les caractères URL-safe par leurs jumelles standard, et comblez le padding manquant. Ce petit assistant est le décodeur base64url fait main le plus courant dans le code C#, et il vaut la peine de le connaître parce qu'il fonctionne sur tous les runtimes depuis .NET Framework 1.1 :
using System;
using System.Text;
string segment = "TWE";
segment = segment.Replace('-', '+').Replace('_', '/');
segment += new string('=', (4 - segment.Length % 4) % 4);
byte[] bytes = Convert.FromBase64String(segment);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// Ma
La formule (4 - length % 4) % 4 est toute l'arithmétique du padding : elle ajoute zéro, un ou deux caractères = pour que la longueur atterrisse sur un multiple de quatre, et le modulo externe empêche une entrée déjà pourvue de padding d'en gagner encore.
Des octets aux mots : texte, Unicode et charsets
Le décodage vous donne des octets, et les octets sont une chose parfaitement neutre. Ils ne deviennent du « texte » que quand vous choisissez un charset pour les lire, et ce choix vous revient, parce que le Base64 ne porte aucune information sur le charset utilisé par l'auteur original. En pratique cela veut dire : supposez UTF-8 sauf si vous avez une raison de ne pas le faire, et soyez explicite dans le code, parce qu'un appel explicite à Encoding.UTF8 fait la différence entre un programme qui est correct par hasard et un qui est correct par conception :
using System;
using System.Text;
string original = "h\u00e9llo \u4e16\u754c";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
string packed = Convert.ToBase64String(utf8);
byte[] decoded = Convert.FromBase64String(packed);
string restored = Encoding.UTF8.GetString(decoded);
Console.WriteLine(restored == original);
// True: h\u00e9llo \u4e16\u754c passe l'aller-retour sans perte
Le piège subtil, c'est ce qui arrive quand les octets ne sont pas du UTF-8 valide, parce que le payload était vraiment du Latin-1, ou binaire, ou tout simplement corrompu. Par défaut, le décodeur UTF-8 de .NET remplace chaque séquence malformée par le caractère de remplacement Unicode (U+FFFD) et passe son chemin. Aucune exception, aucun avertissement : les données sont simplement parties, transformées en points d'interrogation dans votre base de données. Si vous avez besoin de savoir quand cela arrive, construisez l'encodage avec un fallback strict, qui transforme le remplacement silencieux en une DecoderFallbackException retentissante :
using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // les octets FF FE, pas du UTF-8 valide
Encoding strictUtf8 = Encoding.GetEncoding(
"utf-8",
new EncoderExceptionFallback(),
new DecoderExceptionFallback());
string text = strictUtf8.GetString(bytes);
// Lève DecoderFallbackException, car FF FE n'est pas une séquence UTF-8
Pour les payloads où vous préférez survivre plutôt qu'échouer, les fallback de remplacement sont l'option plus douce, et c'est vous qui choisissez le texte de remplacement :
using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // les octets FF FE, pas du UTF-8 valide
Encoding forgivingUtf8 = Encoding.GetEncoding(
"utf-8",
EncoderFallback.ReplacementFallback,
new DecoderReplacementFallback("[bad]"));
string text = forgivingUtf8.GetString(bytes);
Console.WriteLine(text);
// [bad][bad] au lieu du remplacement silencieux par U+FFFD
Une autre leçon d'histoire propre à C# : Encoding.Default ne veut pas dire la même chose sur tous les runtimes. Sur .NET Framework sous Windows, c'est la page de codes ANSI du système (souvent Windows-1252), tandis que sur .NET (Core), c'est du UTF-8 sans BOM. Un code qui fait un aller-retour d'un payload via Encoding.Default peut donc produire des octets différents sur une machine de 2010 et une de 2025, et le Base64 encodera gaiement l'ensemble que vous lui remettrez. Si vous voyez jamais une chaîne décodée pleine de mojibake accentué, Encoding.Default est le premier endroit à regarder.
Fichiers et payloads binaires
Les fichiers sont la cible de décodage la plus simple, parce qu'il n'y a aucune question de charset du tout : les octets que vous décodez sont le fichier, octet par octet, zéros compris. Le motif tient en deux appels et un fichier, et il apparaît partout, des téléversements d'images aux outils de sauvegarde :
using System.IO;
string b64 = File.ReadAllText("payload.b64");
byte[] original = Convert.FromBase64String(b64);
File.WriteAllBytes("restored.bin", original);
Console.WriteLine("Restored " + original.Length + " bytes.");
Deux notes pratiques. Si le fichier peut contenir des espaces blancs ou des sauts de ligne (ce qu'il fait presque certainement, étant un fichier texte), le décodeur classique s'en charge gratuitement, comme vous l'avez vu plus tôt. Et si le payload est grand, ne passez pas par une chaîne du tout : sautez l'étape fichier-vers-chaîne et décodez directement depuis le flux, ce qui fait l'objet de la section suivante. Pour un payload décodé qui est du texte et dont vous connaissez le charset par hasard, l'exemple fichier est toute la solution, et l'étape Encoding.UTF8.GetString de la section charset se glisse exactement entre le décodage et l'utilisation.
Décoder depuis un flux : FromBase64Transform
Les méthodes Convert sont conçues pour les payloads qui tiennent dans une chaîne, et la documentation officielle le dit en des termes explicites : pour les données en flux, utilisez les classes de transformation. FromBase64Transform fait partie de System.Security.Cryptography depuis .NET Framework 1.1 (2003), et elle se branche sur CryptoStream, le tuyau polyvalent du framework pour transformer les données pendant qu'elles coulent. Tout le décodage de fichier à fichier tient en un montage de quatre lignes :
using System.IO;
using System.Security.Cryptography;
using FileStream source = File.OpenRead("payload.b64");
using FromBase64Transform transform =
new FromBase64Transform(FromBase64TransformMode.IgnoreWhiteSpaces);
using CryptoStream reader = new CryptoStream(source, transform, CryptoStreamMode.Read);
using FileStream target = File.Create("payload.bin");
reader.CopyTo(target);
Console.WriteLine("Done, " + target.Length + " bytes written.");
Le constructeur prend un mode, et les deux modes valent la peine d'être connus par leur nom. IgnoreWhiteSpaces (la valeur par défaut, assortie à la politique d'espaces blancs du décodeur classique) saute les quatre espaces blancs ordinaires au fur et à mesure que le flux coule, c'est ce que vous voulez pour des payloads enveloppés en e-mail ou parsemés de sauts de ligne. DoNotIgnoreWhiteSpaces est strict : le premier caractère hors alphabet qu'il rencontre lève une FormatException, c'est ce que vous voulez quand un espace égaré dans le payload doit être un bug, et pas une hausse d'épaules. Sous le capot, la transformation traite l'entrée par groupes de quatre caractères et rend les trois octets que chaque groupe produit, avec TransformFinalBlock qui gère la queue. Vous appelez rarement ces méthodes vous-même, parce que CryptoStream le fait pour vous, mais le fait des groupes de quatre compte : si vous alimentez jamais la transformation à la main, alimentez-la par multiples de quatre, sinon le dernier groupe partiel finira posé dans le bloc final.
Les JWT : trois segments, un point
Un JSON Web Token est le payload base64url le plus fréquent du développement web C#, et sa forme est trompeusement simple : trois segments séparés par des points. Le premier est l'en-tête encodé, le second le payload encodé (alias claims), et le troisième la signature. Chacun des deux premiers est du base64url d'un document JSON en UTF-8, sans padding, conformément à la spécification JWS. Découper et décoder tient en deux lignes de C# :
using System;
using System.Buffers.Text;
using System.Text;
using System.Text.Json;
string jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl";
string[] parts = jwt.Split('.');
string headerJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[0]));
string payloadJson = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(parts[1]));
using JsonDocument doc = JsonDocument.Parse(payloadJson);
Console.WriteLine(doc.RootElement.GetProperty("name").GetString());
// Ada
Sur les runtimes antérieurs à .NET 9, le même travail passe par l'assistant de normalisation de la section URL-safe : replacez - et _ par + et /, apportez le segment à un multiple de quatre, et décodez avec Convert.FromBase64String. Les deux approches vous donnent le même JSON ; choisissez celle qui correspond à votre framework cible.
Une limite à garder bien nette : décoder un JWT, ce n'est pas vérifier un JWT. Le décodage ci-dessus lira gaiement les claims d'un jeton avec une signature n'importe quoi, parce que la signature est une vérification cryptographique séparée sur les deux premiers segments. Pour un travail de jetons en production, ne parsez pas à la main du tout : le paquet System.IdentityModel.Tokens.Jwt (de la famille Microsoft.IdentityModel) s'occupe du parsing, de la validation et de l'expiration en une seule fois, et sa gestion du base64url est exactement l'alphabet que cette section décrit. Décodez à la main pour le débogage et les petits utilitaires ; vérifiez avec la bibliothèque pour tout ce qu'un utilisateur peut atteindre.
Data URIs et images intégrées
Il existe toute une classe de code C# dont le travail est de recevoir un URI data:, parce que HTML, CSS et une grande quantité d'API web les utilisent pour intégrer du contenu binaire inline. Le schéma, standardisé par la RFC 2397, est data:[mediatype][;base64],payload : tout ce qui est avant la première virgule est de la métadonnée (le type MIME et le drapeau ;base64), tout ce qui est après est le payload. Quand le drapeau ;base64 est présent, le payload est une chaîne Base64, et couper à la virgule est tout le parsing :
using System;
using System.Text;
string dataUri = "data:image/png;base64,iVBORw0KGgo=";
int comma = dataUri.IndexOf(',');
string mediaType = dataUri[..comma]; // data:image/png;base64
string b64 = dataUri[(comma + 1)..]; // iVBORw0KGgo=
byte[] imageBytes = Convert.FromBase64String(b64);
Console.WriteLine(imageBytes.Length);
// 8: les octets de signature PNG 89 50 4E 47 0D 0A 1A 0A
Le préfixe iVBORw0KGgo= de l'exemple est la forme Base64 du nombre magique PNG de huit octets, et c'est une empreinte utile : tout data URI pour un vrai PNG commence ainsi, donc c'est un contrôle de bon sens rapide quand vous parsez du HTML non fiable. Deux notes pratiques pour les développeurs C#. D'abord, la classe Uri comprend les data URIs nativement sur .NET : new Uri("data:text/plain;base64,TWFu") se parse sans problème et rapporte Scheme == "data", donc si votre code achemine par URI, les data URIs apparaîtront dans le pipeline et vous devriez décider comment les traiter. Ensuite, souvenez-vous de ce qu'est vraiment un data URI : une copie complète du fichier, gonflée d'un tiers, posée dans votre document. C'est correct pour un favicon de 4 Ko et douloureux pour un logo de 4 Mo, donc quand vous êtes celui qui les génère (l'article encodage couvre ce côté), dimensionnez l'image avant de l'encoder.
HTTP : Basic Auth et échanges d'API
Le Base64 est tissé dans HTTP au moins à un endroit que vous toucherez dans n'importe quel travail d'API : le schéma d'authentification Basic. Le client envoie Authorization: Basic suivi de l'encodage Base64 de username:password, le tout joint par deux-points. Côté serveur, décoder un en-tête entrant tient donc en ceci : retirez le préfixe Basic , décodez, et découpez au premier deux-points :
using System;
using System.Text;
string header = "Basic YWRhOnMzY3JldA==";
string encoded = header["Basic ".Length..].Trim();
string credentials = Encoding.UTF8.GetString(Convert.FromBase64String(encoded));
int colon = credentials.IndexOf(':');
string user = credentials[..colon];
string password = credentials[(colon + 1)..];
Console.WriteLine(user); // ada
Console.WriteLine(password); // s3cret
L'étape UTF-8 compte plus qu'elle n'y paraît : la RFC 7617 ne fixe pas réellement le charset, laissant la valeur par défaut indéfinie pour la compatibilité descendante et ne tolérant qu'un indice UTF-8 consultatif, mais c'est exactement cet indice que tous les serveurs modernes attendent, donc un nom d'utilisateur avec un caractère accentué produit une chaîne d'octets différente (et correcte) du même nom d'utilisateur lu en Latin-1. Le côté décodage de l'authentification Basic est l'extrémité simple de ce motif ; dans ASP.NET Core, vous le croiserez le plus souvent à travers les gestionnaires d'authentification plutôt que des en-têtes bruts, mais la même logique de décodage est ce qu'ils exécutent en dessous, et c'est exactement le genre de code dont vous avez besoin quand vous écrivez des tests d'intégration qui font semblant d'être un serveur d'API. L'opération miroir, construire l'en-tête côté client, tient en une ligne côté encodage, et elle reçoit un exemple complet dans l'article encodage.
E-mail : MIME et payloads enveloppés par lignes
L'e-mail est l'endroit où le Base64 a mérité sa réputation, et c'est encore la source de beaucoup des payloads que reçoivent les services C#. Le SMTP était à l'origine un protocole 7 bits, donc les pièces jointes binaires ne peuvent pas voyager brutes : la spécification MIME (RFC 2045) les encode en Base64 avec un en-tête Content-Transfer-Encoding: base64, enveloppe la sortie à 76 caractères, et sépare les lignes par des paires retour chariot-saut de ligne. Un vrai corps de pièce jointe ressemble donc à une colonne de lignes de 76 caractères, et la bonne nouvelle pour C# est que le décodeur classique sait déjà le lire : parce qu'il saute les espaces blancs n'importe où dans la chaîne, vous pouvez lui remettre tout le corps enveloppé, sauts de ligne et tout, et il le décode comme si les sauts de ligne n'avaient jamais existé :
using System;
using System.Text;
string attachmentBody = "TWFu\r\nTWFu\r\nTWFu";
byte[] bytes = Convert.FromBase64String(attachmentBody);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// ManManMan
Pour les payloads qui arrivent par un flux plutôt que par une chaîne, FromBase64Transform avec son mode qui ignore les espaces blancs est la même histoire en costume de streaming. Et quand vous avez besoin de faire plus que décoder le corps, quand vous devez parcourir la structure MIME, parser des en-têtes, gérer des sections multipart imbriquées, ou extraire chaque pièce jointe d'un vrai fichier .eml, la réponse de l'écosystème en C# est le paquet MimeKit : c'est la bibliothèque MIME standard de .NET, elle gère en interne les encodages de transfert de contenu Base64 et quoted-printable, et c'est l'outil à prendre dès que « ne décoder que le corps » cesse de décrire votre problème. La classe MailMessage du framework lui-même vous décodera les pièces jointes simples, mais son support du MIME est délibérément modeste aux standards modernes.
Les certificats PEM
Le PEM est le format blindé du monde TLS : un corps Base64 entre les marqueurs -----BEGIN CERTIFICATE----- et -----END CERTIFICATE-----, enveloppé à 64 caractères, comme le spécifie la RFC 7468. Les développeurs C# le croisent sous forme des fichiers de certificats derrière chaque point d'accès HTTPS, et l'histoire du décodage ici est meilleure que vous ne l'auriez imaginé, parce que depuis .NET 6 le framework parse le PEM pour vous, corps Base64 et tout :
using System.IO;
using System.Security.Cryptography.X509Certificates;
string pem = File.ReadAllText("server.pem");
X509Certificate2 certificate = X509Certificate2.CreateFromPem(pem);
Console.WriteLine(certificate.Subject);
// CN=server.example.com
Aucun Base64 manuel nulle part dedans : CreateFromPem trouve les marqueurs, déplie le corps, le décode, et vous remet un certificat vivant. (La famille a des sœurs pour les clés privées et pour la forme combinée certificat-plus-clé, si votre infrastructure vous les remet.) Si vous êtes sur un runtime plus ancien, ou si vous avez besoin des octets DER bruts qui se tiennent à l'intérieur de l'armure, la version manuelle tient en deux étapes, retirer l'armure puis décoder, et elle vaut la peine d'être connue parce que le même motif fonctionne pour n'importe quoi de blindé en PEM :
using System;
using System.Text;
string pem = File.ReadAllText("server.pem");
string body = pem
.Replace("-----BEGIN CERTIFICATE-----", "")
.Replace("-----END CERTIFICATE-----", "")
.Replace("\r", "")
.Replace("\n", "");
byte[] der = Convert.FromBase64String(body);
Console.WriteLine(der.Length);
// La longueur du certificat DER à l'intérieur de l'armure
Les pièges de ce coin sont tous des espaces blancs : les fichiers PEM portent des fins de ligne CRLF depuis la plupart des outils de certificat, donc retirez à la fois \r et \n avant de décoder, et pas seulement les sauts de ligne. Et ne confondez pas le corps d'un certificat avec le corps d'une clé privée, qui a des marqueurs différents et un contenu différent ; un décodeur ne vous sauvera pas de celui-là.
Configuration, variables d'environnement et bases de données
La troisième maison du Base64 dans les applications C# est le stockage : fichiers de configuration, variables d'environnement et colonnes de base de données. Le motif est le même partout. Une valeur binaire ou secrète est encodée en chaîne à l'entrée, et décodée en octets à la sortie. Les variables d'environnement sont l'exemple le plus visible, parce qu'elles ne peuvent contenir que du texte :
using System;
using System.Text;
string? encoded = Environment.GetEnvironmentVariable("API_KEY_B64");
if (encoded == null)
{
throw new InvalidOperationException("Set the API_KEY_B64 environment variable first.");
}
byte[] keyBytes = Convert.FromBase64String(encoded);
string apiKey = Encoding.UTF8.GetString(keyBytes);
Console.WriteLine(apiKey.Length + " characters of API key, ready to use.");
Dans une base de données, la même idée apparaît le plus souvent comme une propriété byte[] que vous voulez stocker dans une colonne texte pour la portabilité, 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 en transparent à chaque lecture et écriture :
using Microsoft.EntityFrameworkCore;
modelBuilder.Entity<Avatar>()
.Property(a => a.ImageData)
.HasConversion(
v => Convert.ToBase64String(v),
v => Convert.FromBase64String(v));
Ce seul convertisseur est toute l'intégration base de données : ImageData reste un byte[] dans votre code C#, et la base de données voit une chaîne Base64. Deux mises en garde appartiennent à cette section. D'abord, une colonne de largeur donnée contient environ un tiers de données de moins en texte encodé qu'en binaire brut, à cause de l'impôt des 4 caractères pour 3 octets, donc dimensionnez la colonne pour la longueur encodée si elle est à largeur fixe. Ensuite, et c'est celle de la sécurité : le Base64 dans un fichier de configuration est un confort pour garder une valeur sur une seule ligne, pas une protection de la valeur. Quelconque qui peut lire le fichier de configuration peut décoder la clé en une commande, c'est pour cela que les vrais secrets appartiennent à un coffre-fort de secrets, et le Base64 là-dedans n'est que le format de transport.
Quand le payload est grand
Le décodage Base64 a une propriété agréable que l'encodage n'a pas : la sortie est toujours plus petite que l'entrée, à peu près trois quarts. Un payload texte de 10 Mo se décode en environ 7,5 Mo d'octets, donc un décodage ne peut jamais gonfler votre mémoire comme peut le faire un encodage. L'arithmétique, si vous devez dimensionner un tampon d'avance, se ramène à l'un de deux appels : Base64.GetMaxDecodedFromUtf8Length pour la classe span stricte, ou la simple division, length / 4 * 3 pour l'API classique, plus une marge pour les espaces blancs si l'entrée est enveloppée. (L'assistant renvoie la longueur décodée maximale possible : la vraie longueur lui est égale seulement quand le dernier groupe n'a pas de padding, et est inférieure d'un ou deux octets quand il finit par un ou deux caractères de padding.)
Quand le payload est vraiment grand, par contre, le bon coup n'est pas un tampon plus grand - c'est pas de tampon du tout : sautez la chaîne entièrement et laissez FromBase64Transform streamer le décodage de la source vers la cible, comme montré dans la section flux. La seule règle à respecter est l'alignement sur les groupes de quatre : un flux Base64 ne peut être découpé qu'à des multiples de quatre caractères (après avoir compté les espaces blancs), donc si vous alimentez jamais la transformation à la main, lisez par morceaux qui sont des multiples de quatre et laissez TransformFinalBlock vider le reste. Pour tout ce qui est en deçà de centaines de méga-octets, le décodage one-shot est assez rapide pour que ce soit une optimisation, et pas une nécessité, mais la forme en flux est aussi celle qui se comporte bien sous des limites de mémoire, qui sont exactement les environnements où aiment vivre les grands payloads.
Un décodeur dans votre terminal
Il y a un moment satisfaisant, dans chaque langage, où un programme console de 15 lignes devient un outil de ligne de commande, et le décodeur Base64 de C# est un bon candidat pour le faire, parce que lire depuis l'entrée standard en fait un remplaçant prêt à l'emploi pour les tuyaux du shell. Voici l'outil entier : il lit le payload Base64 depuis le tuyau (ou depuis un argument), le décode, et écrit les octets bruts dans un fichier :
using System;
using System.IO;
using System.Text;
string input = args.Length > 0 ? File.ReadAllText(args[0]) : Console.In.ReadToEnd();
byte[] bytes = Convert.FromBase64String(input.Trim());
File.WriteAllBytes("output.bin", bytes);
Console.Error.WriteLine("Wrote " + bytes.Length + " bytes to output.bin.");
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 décodeur du runtime .NET : faites passer un fichier dedans, enchaînez-le avec d'autres outils, et les règles de validation strictes de C# (tolérantes aux espaces blancs, strictes sur l'alphabet, strictes sur le padding) deviennent partie de votre pipeline. Le Trim() fait du travail discret là, attrapant le saut de ligne final que les éditeurs de texte aiment ajouter, bien que, pour être juste, le décodeur l'aurait ignoré de toute façon. Pour les payloads URL-safe qui apparaissent de plus en plus dans les logs d'API, le même squelette avec le décodage Base64Url de la section URL-safe est tout le changement.
Vitesse : à quoi s'attendre
Le Base64 dans .NET moderne est rapide, et il devient de plus en plus rapide. Les implémentations du runtime des méthodes Convert comme des classes System.Buffers.Text sont optimisées avec des instructions vectorielles SIMD quand le matériel les supporte, et elles traitent de nombreux caractères par cycle. En pratique cela veut dire que des payloads de plusieurs méga-octets se décodent en quelques millisecondes à une quinzaine de millisecondes sur une machine de bureau ordinaire, ce qui est assez rapide pour que le décodage Base64 soit effectivement gratuit dans n'importe quelle application que vous écrivez. Le conseil de performance pratique porte donc sur la forme de votre code, et non sur le décodeur lui-même. Préférez les méthodes Try ou les méthodes span qui renvoient un statut sur les chemins critiques, où une entrée malformée est possible et où les exceptions seraient coûteuses. Réutilisez les tampons avec les API in-place et span quand vous décodez des milliers de petits payloads dans une boucle, au lieu d'allouer un tableau neuf par appel. Et ne décodez jamais deux fois le même payload : une fois, c'est le coût, et un second décodage d'un champ que vous avez déjà décodé est du gaspillage pur qui apparaît dans les profils comme un mystérieux second pic Base64.
Sécurité : ce que Base64 ne fait pas
Le fait de sécurité le plus important sur le Base64 est celui que les débutants ratent le plus souvent : c'est un encodage, pas un chiffrement. Une chaîne Base64 est lisible par n'importe qui, avec n'importe quel outil, en une fraction de seconde, et C# en fait une lecture d'une seule ligne, comme tout cet article l'a démontré. Le Base64 n'a pas de clé, pas de paramètre d'algorithme, et pas de faiblesse à exploiter, parce qu'il n'a jamais tenté de cacher quoi que ce soit : c'est un format de transport, une façon de faire survivre le binaire dans des canaux texte uniquement. Traitez-le en conséquence. 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 Convert.FromBase64String. Si la valeur doit être secrète, elle a besoin d'une vraie protection (un gestionnaire de secrets, un stockage chiffré, à tout le moins un contrôle d'accès du système d'exploitation), et le Base64 n'est que la forme qu'elle porte en voyage.
La deuxième note de sécurité porte sur votre propre chemin de décodage. Chaque payload que vous décodez est une entrée non fiable jusqu'à preuve du contraire, et les deux modes d'échec pour lesquels concevoir sont le bruyant (entrée invalide, à laquelle l'API classique répond d'une FormatException que vous devez attraper et convertir en 400, et non en 500) et le silencieux (du Base64 valide qui se décode en des octets qui ne sont pas ce que vous attendiez : pas du UTF-8, pas le type de fichier que vous demandiez, ou plus long que votre budget). Validez avant de faire confiance : vérifiez la longueur avec IsValid ou la famille Try avant d'allouer, vérifiez les octets décodés contre une signature attendue (le nombre magique PNG, l'en-tête PKCS) avant de les remettre à un parseur d'image ou de certificat, et dimensionnez vos tampons depuis la longueur encodée avant de décoder, et non après. Le Base64 décodera n'importe quoi de bien formé ; décider ce que bien formé signifie pour votre application, c'est votre travail.
Les pièges à connaître avant qu'ils ne mordent
Ce sont les pièges propres à C# qui reviennent sans cesse dans le code réel, et chacun d'eux a une cause concrète dans la façon dont le framework fonctionne :
- Le binaire à travers une chaîne. Une
stringC# est une suite d'unités de code UTF-16, et le Base64 décodé ne l'est pas. Au moment où vous bourrez des octets décodés dans une variable chaîne (unConsole.WriteLined'un PNG décodé, une concaténation de chaînes avec du binaire, une bibliothèque JSON qui sérialise du « texte »), quelque chose en aval va le déformer. Gardez le binaire décodé dans unbyte[]jusqu'à ce qu'il atteigne un endroit qui veut vraiment des octets. - La divergence d'Encoding.Default. Le code qui lit des octets décodés avec
Encoding.Defaultproduit du texte différent sur .NET Framework (la page de codes ANSI de Windows) et sur .NET (UTF-8). Le même payload, deux sorties différentes, aucune exception. Fixez votre encodage explicitement. - Segments JWT et décodeur classique. Remettre un segment JWT brut à
Convert.FromBase64Stringéchoue de deux façons à la fois : les caractères-/_sont hors de l'alphabet standard, et le padding manquant brise la règle de longueur. Normalisez d'abord, ou utilisezBase64Url. - Les espaces blancs que vous voyez et ceux que vous ne voyez pas. Le décodeur saute l'espace, la tabulation, le saut de ligne et le retour chariot, et ne saute rien d'autre. Un espace insécable, un séparateur de ligne Unicode, ou une tabulation verticale dans un payload (tous capables de survivre à un copier-coller depuis certaines pages web) est une
FormatException, pas une hausse d'épaules. - Un message d'erreur pour tous les crimes. La
FormatExceptiondu décodeur classique ne dit pas quelle règle a cassé ni où. Déboguez en vérifiant la longueur, puis l'alphabet, puis le padding, dans cet ordre, ou passez àTryFromBase64StringetIsValidpour une réponse booléenne. - Le remplacement UTF-8 silencieux.
Encoding.UTF8.GetStringtransforme les séquences d'octets malformées en U+FFFD sans se plaindre. Si le payload n'est peut-être pas du UTF-8 valide, utilisez le fallback strict de la section charset, sinon vous serez en train d'investiguer des données manquantes des semaines après les faits. - Couper le flux au mauvais endroit. Un flux Base64 ne peut être coupé qu'à des multiples de quatre caractères. Coupez un décodage en flux à n'importe quelle autre frontière et le dernier groupe partiel atterrit dans
TransformFinalBlock, où il est soit à sa place, soit il casse votre comptabilité d'alignement. - Les fins de ligne PEM. Les fichiers de certificat portent du CRLF. Retirez
\rainsi que\nquand vous dépliez l'armure à la main, sinon la première ligne de votre DER « décodé » est un retour chariot qui porte les vêtements d'un octet de données. - L'encodage double. Si un payload était déjà du Base64 quand il vous est arrivé (une configuration qui a encodé en Base64 une chaîne Base64, une API qui a encodé la sortie d'un autre encodeur), un décodage vous donne plus de Base64, pas vos données. L'aller-retour ne se referme qu'après autant de décodages qu'il y a eu d'encodages, et le côté encodeur de ce bug fait l'objet de l'article encodage.
Une brève histoire du Base64 en C#
L'histoire du Base64 en C# est aussi l'histoire de la plateforme .NET qui grandit, et elle est plus longue que la plupart ne le pensent :
- .NET Framework 1.1, avril 2003.
Convert.FromBase64Stringet ses sœurs arrivent, et elles portent le design qui définit encore l'API : strict sur l'alphabet, généreux sur les quatre espaces blancs, direct sur ses erreurs. Pendant l'essentiel des deux décennies suivantes, cette seule méthode est le décodeur Base64 en C# par excellence. - .NET 2.0, 2005. L'énumération
Base64FormattingOptionsrejointConvert, apportant les sauts de ligne façon MIME au côté encodage (et la tolérance aux espaces blancs assortie au côté décodage, où elle travaille déjà discrètement). - .NET Core 2.1, 2018. L'ère des span.
Convertgagne les méthodesTryet un encodage par span, et la nouvelle classeSystem.Buffers.Text.Base64arrive avec son contratOperationStatus, son décodage in-place etIsValid, construite pour le monde zéro-allocation de la réécriture centrée sur la mémoire. - .NET 5, 2020. Les sœurs hexadécimales (
Convert.ToHexStringet compagnie) sortent, le même motif de design que le Base64 appliqué à un alphabet de 16 symboles, un signe que le motif de la classe de conversion était devenu un style de maison. - .NET 6, 2021.
X509Certificate2.CreateFromPemfait du PEM une entrée de premier ordre, et toute une classe de code qui retire l'armure à la main devient optionnelle sur les runtimes modernes. - .NET 9, novembre 2024.
System.Buffers.Text.Base64Urlatterrit enfin dans la boîte après des années de demandes de la communauté, et le paquetMicrosoft.Bcl.Memoryle rétroporte vers .NET Framework 4.6.2 et plus pour les bases de code legacy qui font encore tourner tout. - .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 lente marche vers une surface plus ergonomique.
Vaut la peine de garder en tête : l'encodage lui-même est bien plus vieux que tout cela. 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), le MIME a standardisé 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. Le C# a hérité de tout : chaque bizarrerie d'enveloppement de lignes et de padding que vous rencontrez dans un format d'e-mail vieux de 30 ans est une bizarrerie que le décodeur C# a été conçu pour absorber.
Faits C# curieux
- Le plus petit test de fumée.
"TWFu"se décode enMan. Trois octets, pas de padding, pas d'excuses. C'est le hello world du débogage Base64 en C#, et il exerce tout le chemin du succès en quatre caractères. - Un décodeur avec un historique postal. La tolérance aux espaces blancs n'est pas un accident d'implémentation - c'est une décision de design héritée du MIME : tout un corps d'e-mail enveloppé en lignes de 76 caractères, avec toutes ses paires CRLF, est un argument unique valide pour
Convert.FromBase64String. Le décodeur a été construit pour manger le format que l'e-mail utilise depuis trente ans. - Une erreur, trois causes. Le message classique de
FormatExceptionénumère les trois modes d'échec qu'il pourrait signaler (mauvais caractère, trop de padding, padding mal placé) et ne dit pas lequel s'est produit. C'est le seul message d'erreur de la surface API qui fonctionne comme une question à choix multiples. - Un espace de noms qui ment un peu.
System.Buffers.Textsonne comme s'il s'agissait de traitement de texte, mais c'est en réalité la maison de la conversion binaire-vers-texte en général : lesUtf8ParseretUtf8Formatterqui analysent des nombres et des dates directement en UTF-8 vivent tout près des classes Base64. - Le padding est optionnel d'un côté de la famille. La classe
Base64UrldécodeAQIDBA(six caractères, sans padding) etAQIDBA==(les mêmes octets avec padding) vers les mêmes quatre octets, tandis que le décodeur classique n'accepte que la forme avec padding. Deux décodeurs, deux contrats, un runtime. - Des chaînes qui ne devraient pas exister. Une chaîne C# peut légalement contenir des octets NUL, donc
Encoding.UTF8.GetStringsur du binaire décodé peut produire une « chaîne » pleine de caractères de contrôle que la console, votre écriture CSV et la moitié des bibliothèques JSON de la planète géreront chacun différemment. Le système de types le permet ; l'écosystème, presque pas. - Un vestige 1.1 en bonne posture.
Convert.FromBase64CharArraya la même signature à trois paramètres depuis avril 2003, survivant à la révolution des generics, à la révolution des span et à la révolution URL-safe sans une seule surcharge ajoutée. L'ère des tableaux de char en C# n'est pas partie ; elle se repose juste. - Onze caractères, huit octets. Les identifiants vidéo de YouTube sont du base64url sans padding : 11 caractères qui se décodent en 8 octets.
Base64Url.GetMaxDecodedLength(11)vous dit le 8, et le décodage tient en une ligne, ce qui est une belle façon de finir la journée si vous êtes le genre de personne qui écrit ce genre de choses.
L'autre direction
Voilà le côté décodeur, et c'est là que vit l'essentiel de la douleur, parce que le décodage est l'endroit où vous rencontrez les données des autres : leurs choix de padding, leurs sauts de ligne, leurs alphabets, leurs jetons. La direction opposée, prendre vos propres octets et les emballer en Base64, est un problème plus paisible avec son propre lot de décisions à prendre, et son propre lot de pièges. L'encodage Base64 en C#, de la question des 76 caractères aux jetons URL-safe, est traité en profondeur dans l'article compagnon lié ci-dessous, et c'est une lecture courte et satisfaisante une fois qu'on sait sur quoi porter son regard.
Dernière mise à jour : 2026-09-08
Article associé : Encodage Base64 en C# (CSharp) : un guide complet