Decodifica Base64 in C# (CSharp): una guida completa
Lo riconosci in un istante: un fiume di lettere e cifre, ogni tanto un + o un /, e magari un = o due appesi alla fine. Da qualche parte tra una risposta di API, un allegato email, un file di configurazione e un JWT, qualcuno ha impacchettato dati binari dentro un testo, e ora tocca a te aprirlo. Questo è il lato decodifica del Base64 in C#, e la prima buona notizia è che non ti serve nulla oltre al framework. Il decoder vive nell'namespace System da più di vent'anni, e ogni runtime .NET moderno lo spedisce ancora, con più opzioni e prestazioni migliori rispetto all'originale.
Un rapido ripasso, perché la home page di questo sito spiega il formato nel dettaglio: quattro caratteri di un alfabeto di 64 simboli portano tre byte di dati, e uno o due = in coda marcano i byte rimasti. La decodifica rifà questo scambio al contrario, quindi il risultato è circa tre quarti della dimensione dell'input. Con la forma del problema in mente, apriamo qualche pacchetto.
La famiglia dei decoder: conosci le tue opzioni
Prima del primo esempio, ecco tutta la famiglia di API di decodifica a tua disposizione, e la situazione per cui ogni singola API è stata costruita. Tutto quanto è elencato qui fa parte del runtime .NET stesso, a eccezione della classe URL-safe sui framework più vecchi, che viaggia dentro un piccolo pacchetto NuGet:
| API | Disponibile da | A cosa serve |
|---|---|---|
Convert.FromBase64String(string) |
.NET Framework 1.1 (2003) | Il classico. Una stringa in entrata, un byte[] fresco in uscita. Salta gli spazi bianchi ordinari, lancia un'eccezione per tutto il resto. |
Convert.FromBase64CharArray(char[], int, int) |
.NET Framework 1.1 (2003) | La stessa decodifica, leggendo da una fetta di un buffer di caratteri che possiedi già. |
Convert.TryFromBase64String, Convert.TryFromBase64Chars |
.NET Core 2.1 (2018) | Un booleano al posto delle eccezioni, scrittura in uno span che fornisci tu. La guardia amichevole per gli input non fidati. |
System.Buffers.Text.Base64 |
.NET Core 2.1 (2018) | L'API span rigorosa: codici di stato al posto delle eccezioni, decodifica in loco e pre-controlli IsValid. |
System.Buffers.Text.Base64Url |
.NET 9 (2024) | L'alfabeto URL-safe (- e _ al posto di + e /), con o senza padding. Su .NET Framework 4.6.2+ e .NET Standard 2.0: il pacchetto NuGet Microsoft.Bcl.Memory. |
FromBase64Transform + CryptoStream |
.NET Framework 1.1 (2003) | Decodifica in streaming: da file a file, dalla rete al disco, pezzo per pezzo, senza caricare l'intero payload. |
Se il tuo progetto punta a una versione .NET dal 2018 in poi, le prime quattro righe sono già di serie. Base64Url richiede .NET 9 o successivo, oppure il pacchetto Microsoft.Bcl.Memory su tutto ciò che è più vecchio. E una nota in avanti: le librerie .NET 11, in preview al momento della scrittura con un rilascio generale atteso per la fine del 2026, aggiungono altre API di comodità Base64 e sovraccarichi ai tipi esistenti, quindi la famiglia continua a crescere. Nient'altro in questo articolo richiede un pacchetto.
Il cavallo da lavoro: Convert.FromBase64String
Novanta per cento della vita di decodifica in C# è una singola chiamata. Gli passi una stringa, e ti restituisce i byte esatti che erano impacchettati dentro:
using System;
using System.Text;
string packed = "TWFu";
byte[] bytes = Convert.FromBase64String(packed);
string text = Encoding.UTF8.GetString(bytes);
Console.WriteLine(text);
// Man
Tre dettagli valgono di essere memorizzati. Primo, il valore restituito sono byte, non testo: è un byte[], il decoder è orientato ai byte da capo a coda, ed è esattamente ciò che vuoi, perché il payload può essere una frase, un PNG, un certificato o un hash, e nessuno di essi dovrebbe essere trattato come un caso speciale. Il salto dai byte di nuovo a un testo leggibile è un passo separato e deliberato attraverso Encoding, ed è lì che vivono le decisioni sul charset (ne parleremo più avanti). Secondo, il decoder alloca un array fresco a ogni chiamata, dimensionato alla lunghezza decodificata, quindi non ti restituisce mai un buffer con capacità in eccesso. Terzo, il contratto è piccolo e onesto: una stringa vuota si decodifica in un array vuoto, un riferimento null lancia ArgumentNullException, e qualsiasi cosa che non sia Base64 valido lancia FormatException. Tutto il resto è un'elaborazione di queste tre regole.
Ciò che perdona e ciò che rifiuta
È qui che il decoder di C# ha una personalità, e non da poco. È generoso su esattamente una cosa - gli spazi bianchi - e spietato su tutto il resto. Il decoder salta esattamente quattro caratteri, ovunque appaiano nella stringa: lo spazio (U+0020), la tabulazione (U+0009), l'a capo (U+000A) e il ritorno a capo (U+000D). Questa politica è un riferimento deliberato alla posta elettronica, dove i payload Base64 arrivano avvolti in righe di 76 caratteri, e significa che un allegato avvolto in MIME si decodifica con zero pre-elaborazione. Qualsiasi cosa fuori dall'alfabeto di 64 simboli, qualsiasi cosa che violi le regole sulla lunghezza, o qualsiasi cosa con il padding nel posto sbagliato si becca un'eccezione. Guarda lo stesso decoder all'opera su alcuni input diversi:
| Input | Risultato |
|---|---|
"TWFu" |
Si decodifica in Man (3 byte). |
"TWF\nu" (un a capo nel mezzo) |
Si decodifica in Man. Gli spazi bianchi sono invisibili al decoder. |
"TWFu\u00A0" (uno spazio non separatore alla fine) |
FormatException. Vengono saltati solo i quattro caratteri di spaziatura di cui sopra; lo NBSP non è uno di questi. |
"TWE" (lunghezza 3, non un multiplo di 4) |
FormatException. La lunghezza del payload, ignorando gli spazi bianchi, deve essere un multiplo di 4. |
"TWFu=" (padding extra dopo i dati) |
FormatException. Al massimo due caratteri di padding, e solo alla fine. |
"-_88" (alfabeto URL-safe) |
FormatException. Il decoder standard conosce solo i 64 caratteri dell'alfabeto standard. |
null |
ArgumentNullException: Value cannot be null. (Parameter 's') |
Un'ultima particolarità da memorizzare: ogni reato di formato riceve lo stesso unico messaggio d'errore, The input is not a valid Base-64 string as it contains a non-base 64 character, more than two padding characters, or an illegal character among the padding characters. Il messaggio elenca tutte e tre le cause possibili e non dice quale hai colpito, e non dice nemmeno dove. Se stai facendo il debug di un payload che fallisce, conta i caratteri, controlla l'alfabeto, e controlla il padding, in quest'ordine.
Decodifica senza eccezioni: le API Try
Un flusso di controllo guidato dalle eccezioni è un pattern legittimo, ma per input ad alto volume o non fidati la famiglia Try si comporta meglio. È stata aggiunta in .NET Core 2.1 e arriva in due varianti: una che legge da una stringa e una che legge da uno span di caratteri. Entrambe scrivono in un buffer che fornisci tu e riportano quanto ne hanno riempito:
using System;
using System.Text;
string payload = "TWFu"; // qualsiasi payload, valido o no
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.");
}
Due comportamenti fanno sembrare le varianti Try una specie a sé stante. Un input invalido restituisce false invece di lanciare un'eccezione, quindi un flusso di payload malformati ti costa una ramificazione e non un'eccezione. Una riserva: un input null non fa parte del contratto - lancia ArgumentNullException - quindi la guardia Try copre i payload rotti, e un valore che potrebbe mancare ha comunque bisogno del suo controllo null prima. Il metodo fratello Convert.TryFromBase64Chars fa lo stesso lavoro da un ReadOnlySpan<char>, il che è comodo quando il payload vive in un più grande buffer di caratteri e non vuoi prima tagliare via una sottostringa. Dimensiona il buffer di uscita con generosità: la lunghezza decodificata vale al massimo tre quarti della lunghezza (senza spazi bianchi) dell'input, e il parametro out written ti dice esattamente quanto ne è uscito.
Decodifica basata sugli span con System.Buffers.Text.Base64
Quando conti le allocazioni, o quando vuoi che il decoder descriva i suoi fallimenti invece di lanciarli, la classe System.Buffers.Text.Base64 è lo strumento giusto. È una classe statica nella libreria standard da .NET Core 2.1, e lavora sugli span invece che sugli array gestiti. Il suo metodo di decodifica restituisce un valore OperationStatus con quattro umori: Done (successo), DestinationTooSmall (il tuo buffer era troppo piccolo), NeedMoreData (l'input non è ancora un multiplo di 4, continua a leggere) e InvalidData (questo non è Base64). L'ultimo parametro booleano, isFinalBlock, è ciò che distingue questi due: dice al decoder se sta arrivando altro input. Ecco la versione in un colpo solo, dimensionata con l'helper della stessa 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
}
Altri due membri di questa classe meritano un paragrafo. Il primo è IsValid, che valida un payload senza decodificarlo. Arriva in due varianti, una per span di byte e una per span di caratteri, e un sovraccarico riporta la lunghezza decodificata insieme al verdetto, così puoi dimensionare un buffer con un singolo controllo:
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.");
}
Il secondo è DecodeFromUtf8InPlace, per la situazione in cui il testo Base64 sta già in un buffer che possiedi e non ti importa di sovrascriverlo. La decodifica rimpicciolisce i dati, quindi il risultato viene scritto all'inizio dello stesso buffer e il metodo riporta quanto è lungo:
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, adesso che vive nei primi tre byte dello stesso buffer
}
Un comportamento da tenere in tasca: questa classe salta anche i quattro caratteri di spaziatura ordinari (spazio, tab, a capo, ritorno a capo), quindi un payload con a capo si decodifica altrettanto bene. È rigorosa dove conta: un payload la cui lunghezza senza spazi bianchi non è un multiplo di quattro è InvalidData quando è il blocco finale, e i caratteri fuori dall'alfabeto standard sono rifiutati a prescindere. Non c'è pulizia silenziosa da nessuna parte in questa classe.
Base64 URL-safe: la classe Base64Url
Esiste un secondo alfabeto per gli stessi 64 valori, e lo incontrerai costantemente nel lavoro web in C#. Nell'alfabeto standard, i valori 62 e 63 sono + e /, due caratteri che danno fastidio negli URL: un + in una query string viene abitualmente decodificato come spazio, e / e = hanno ciascuno bisogno di percent-encoding. RFC 4648, sezione 5, risolve il problema sostituendo - e _, che non hanno alcun significato speciale in nessun contesto URL, e rende facoltativo il padding finale =. Il risultato si chiama base64url, ed è l'alfabeto dei JWT, dei token API, degli ID di upload dei file e di un gran numero di URL (gli identificatori video di YouTube da 11 caratteri sono base64url senza padding).
Da .NET 9 la libreria standard spedisce una classe dedicata: System.Buffers.Text.Base64Url. È la gemella URL-safe della classe Base64, con i suoi helper di decodifica, validazione e lunghezza:
using System.Buffers.Text;
using System.Text;
string token = "-__8";
byte[] bytes = Base64Url.DecodeFromChars(token);
Console.WriteLine(BitConverter.ToString(bytes));
// FB-FF-FC
Notate cosa l'API classica non avrebbe fatto con quell'esempio. Gli stessi tre byte si codificano come +//8 nell'alfabeto standard, e Convert.FromBase64String("+//8") funziona, ma Convert.FromBase64String("-__8") lancia un'eccezione, perché i caratteri URL-safe sono fuori dal suo alfabeto. E i payload base64url arrivano comunemente senza padding, che il decoder classico rifiuta anch'esso, perché esige il gruppo completo di quattro. La classe Base64Url gestisce nativamente entrambe le varianti del problema: decodifica TWE (tre caratteri, senza padding) nei due byte Ma, e decodifica TWE= altrettanto bene.
Se il tuo progetto gira su un runtime più vecchio, ci sono due strade pratiche. Su .NET Framework 4.6.2 e successivi, aggiungi il pacchetto NuGet Microsoft.Bcl.Memory, che Microsoft pubblica specificamente per fare il backport di Base64Url (insieme ad alcuni altri tipi moderni):
dotnet add package Microsoft.Bcl.Memory
Oppure, senza alcun pacchetto, normalizza il payload prima di passarlo al decoder classico: rimetti i caratteri URL-safe sui loro gemelli standard, e integra il padding mancante. Questo piccolo helper è il decoder base64url fatto a mano più comune nel codice C#, e vale la pena conoscerlo perché funziona su ogni runtime da .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 formula (4 - length % 4) % 4 è l'intera aritmetica del padding: aggiunge zero, uno o due caratteri = così la lunghezza atterra su un multiplo di quattro, e il modulo esterno impedisce a un input già con padding di prenderne altri.
Dai byte alle parole: testo, Unicode e charset
La decodifica ti dà byte, e i byte sono una cosa perfettamente neutra. Diventano "testo" solo quando scegli un charset per leggerli, e quella scelta spetta a te, perché il Base64 non porta alcuna informazione sul charset usato dall'autore originale. Nella pratica questo significa: assumi UTF-8 a meno di non avere una ragione per non farlo, e sii esplicito nel codice, perché una chiamata esplicita a Encoding.UTF8 fa la differenza tra un programma corretto per caso e uno corretto per design:
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 fa un'andata e ritorno senza perdite
La trappola sottile è ciò che succede quando i byte non sono UTF-8 valido, perché il payload era davvero Latin-1, o binario, o semplicemente corrotto. Di default, il decoder UTF-8 di .NET sostituisce ogni sequenza malformata con il carattere di sostituzione Unicode (U+FFFD) e prosegue. Nessuna eccezione, nessun avviso: i dati sono semplicemente svaniti, trasformati in punti interrogativi nel tuo database. Se hai bisogno di sapere quando succede, costruisci l'Encoding con un fallback rigoroso, che trasforma la sostituzione silenziosa in una DecoderFallbackException fragorosa:
using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // i byte FF FE, non UTF-8 valido
Encoding strictUtf8 = Encoding.GetEncoding(
"utf-8",
new EncoderExceptionFallback(),
new DecoderExceptionFallback());
string text = strictUtf8.GetString(bytes);
// Lancia DecoderFallbackException, perché FF FE non è una sequenza UTF-8
Per i payload dove preferisci sopravvivere piuttosto che fallire, i fallback di sostituzione sono l'opzione più dolce, e scegli tu il testo di sostituzione:
using System.Text;
byte[] bytes = Convert.FromBase64String("//4="); // i byte FF FE, non UTF-8 valido
Encoding forgivingUtf8 = Encoding.GetEncoding(
"utf-8",
EncoderFallback.ReplacementFallback,
new DecoderReplacementFallback("[bad]"));
string text = forgivingUtf8.GetString(bytes);
Console.WriteLine(text);
// [bad][bad] al posto della sostituzione silenziosa con U+FFFD
Un'altra lezione di storia specifica di C#: Encoding.Default significa cose diverse su runtimes diversi. Su .NET Framework su Windows è la code page ANSI del sistema (spesso Windows-1252), mentre su .NET (Core) è UTF-8 senza BOM. Un codice che fa andare e tornare un payload attraverso Encoding.Default può quindi produrre byte diversi su una macchina del 2010 e una del 2025, e il Base64 codificherà allegramente qualunque insieme gli passi. Se mai vedi una stringa decodificata piena di mojibake accentato, Encoding.Default è il primo posto da guardare.
File e payload binari
I file sono il target di decodifica più diretto, perché la questione del charset non c'è proprio: i byte che decodifichi sono il file, byte per byte, zeri inclusi. Il pattern sono due chiamate e un file, e compare dappertutto, dagli upload di immagini agli strumenti di backup:
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.");
Due note pratiche. Se il file può contenere spazi bianchi o a capo (e, essendo un file di testo, quasi certamente lo fa), il decoder classico se la cava gratis, come hai visto prima. E se il payload è grande, non passare per una stringa affatto: salta il passaggio da file a stringa e decodifica direttamente dal flusso, che è l'argomento della prossima sezione. Per un payload decodificato che è testo e di cui conosci il charset, l'esempio del file è la soluzione intera, e il passo Encoding.UTF8.GetString della sezione sui charset entra a meraviglia tra la decodifica e l'uso.
Decodifica da un flusso: FromBase64Transform
I metodi Convert sono progettati per payload che stanno in una stringa, e la documentazione ufficiale lo dice in questi termini: per i dati in streaming, usa le classi transform. FromBase64Transform fa parte di System.Security.Cryptography dal .NET Framework 1.1 (2003), e si innesta in CryptoStream, il tubo generico del framework per trasformare i dati mentre scorrono. L'intera decodifica da file a file è un setup di quattro righe:
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.");
Il costruttore prende una modalità, e le due modalità valgono la pena di essere conosciute per nome. IgnoreWhiteSpaces (il default, in linea con la politica sugli spazi bianchi del decoder classico) salta i quattro caratteri di spaziatura ordinari man mano che il flusso scorre, ed è ciò che vuoi per i payload avvolti in email o pieni di a capo. DoNotIgnoreWhiteSpaces è rigoroso: il primo carattere fuori alfabeto che incontra lancia una FormatException, ed è ciò che vuoi quando uno spazio intruso nel payload dovrebbe essere un bug, non una spalla da alzare. Sotto il cofano, la transform elabora l'input in gruppi di quattro caratteri e restituisce i tre byte che ogni gruppo produce, con TransformFinalBlock che si occupa della coda. Raramente chiami tu quei metodi, perché CryptoStream lo fa per te, ma il fatto dei gruppi di quattro conta: se mai alimenti la transform a mano, alimentazione in multipli di quattro, oppure l'ultimo gruppo parziale resterà nel blocco finale.
JWT: tre segmenti, un punto
Un JSON Web Token è il payload base64url con più traffico nello sviluppo web in C#, e la sua forma è ingannevolmente semplice: tre segmenti separati da punti. Il primo è l'intestazione codificata, il secondo il payload codificato (alias claims), e il terzo la firma. Ciascuno dei primi due è il base64url di un documento JSON in UTF-8, senza padding, secondo la specifica JWS. Dividere e decodificare sono due righe di 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
Sui runtimes precedenti a .NET 9, lo stesso lavoro passa per l'helper di normalizzazione della sezione URL-safe: rimetti - e _ come + e /, porta il segmento a un multiplo di quattro, e decodifica con Convert.FromBase64String. Entrambi gli approcci ti danno lo stesso JSON; scegli quello che corrisponde al tuo framework target.
Un confine da tenere ben distinto: decodificare un JWT non è verificare un JWT. La decodifica di sopra leggerebbe allegramente i claims di un token con una firma spazzatura, perché la firma è un controllo crittografico separato sui primi due segmenti. Per il lavoro con i token in produzione, non analizzare a mano per niente: il pacchetto System.IdentityModel.Tokens.Jwt (della famiglia Microsoft.IdentityModel) gestisce analisi, validazione e scadenza tutto in uno, e la sua gestione del base64url è esattamente l'alfabeto descritto in questa sezione. Decodifica a mano per il debug e per piccoli strumenti; verifica con la libreria per tutto ciò che un utente può raggiungere.
Data URI e immagini incorporate
C'è un'intera classe di codice C# il cui lavoro è ricevere un URI data:, perché HTML, CSS e un gran numero di API web li usano per incorporare contenuto binario inline. Lo scheme, standardizzato da RFC 2397, è data:[mediatype][;base64],payload: tutto prima della prima virgola è metadati (il tipo MIME e il flag ;base64), tutto dopo è il payload. Quando il flag ;base64 è presente, il payload è una stringa Base64, e dividere alla virgola è l'intera analisi:
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: i byte di firma PNG 89 50 4E 47 0D 0A 1A 0A
Il prefisso iVBORw0KGgo= nell'esempio è la forma Base64 del numero magico PNG di otto byte, ed è un'impronta utile: qualunque data URI per un PNG vero inizia così, quindi è un rapido controllo di sanità quando analizzi HTML non fidato. Due note pratiche per gli sviluppatori C#. Prima, la classe Uri capisce i data URI nativamente su .NET: new Uri("data:text/plain;base64,TWFu") si analizza senza problemi e riporta Scheme == "data", quindi se il tuo codice fa il routing sugli URI, i data URI compariranno nel pipeline e dovrai decidere come gestirli. Secondo, ricorda cos'è davvero un data URI: una copia completa del file, gonfiata di un terzo, che sta dentro il tuo documento. Va bene per un favicon da 4 KB e fa male per un logo da 4 MB, quindi quando sei tu a generarli (l'articolo sulla codifica copre quel lato), dimensiona l'immagine prima di codificarla.
HTTP: autenticazione Basic e scambi API
Il Base64 è tessuto nell'HTTP in almeno un posto che toccherai in qualunque lavoro con le API: lo scheme di autenticazione Basic. Il client invia Authorization: Basic seguito dalla codifica Base64 di username:password, uniti da due punti. Sul lato server, decodificare un header in arrivo è quindi: togliere il prefisso Basic , decodificare, e dividere alla prima virgola:
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
Il passo UTF-8 conta più di quanto sembri: la RFC 7617 non fissa in realtà il charset, lasciando il default indefinito per retrocompatibilità e permettendo solo un suggerimento UTF-8 a titolo consultivo, ma è proprio quel suggerimento che ogni server moderno si aspetta, quindi un nome utente con un carattere accentato produce una stringa di byte diversa (e corretta) rispetto allo stesso nome utente letto come Latin-1. Il lato decodifica dell'autenticazione Basic è l'estremità semplice di questo pattern; in ASP.NET Core lo incontrerai di solito attraverso gli handler di autenticazione piuttosto che con header grezzi, ma la stessa logica di decodifica è ciò che gira sotto, ed è esattamente il tipo di codice che ti serve quando scrivi test di integrazione che fanno da finto server API. L'operazione speculare, costruire l'header sul lato client, è una riga sola sul lato codifica, e riceve un esempio completo nell'articolo sulla codifica.
Email: MIME e payload con a capo
L'email è il posto dove il Base64 si è guadagnato la reputazione, ed è ancora la fonte di molti dei payload che i servizi C# ricevono. Lo SMTP era originariamente un protocollo a 7 bit, quindi gli allegati binari non possono viaggiare grezzi: la specifica MIME (RFC 2045) li codifica come Base64 con un header Content-Transfer-Encoding: base64, avvolge l'output a 76 caratteri e separa le righe con coppie ritorno a capo + a capo. Un corpo di allegato vero ha quindi l'aspetto di una colonna di righe da 76 caratteri, e la buona notizia per C# è che il decoder classico sa già leggerlo: perché salta gli spazi bianchi ovunque nella stringa, puoi passargli l'intero corpo avvolto, a capo e tutto il resto, e lo decodifica come se gli a capo non ci fossero mai stati:
using System;
using System.Text;
string attachmentBody = "TWFu\r\nTWFu\r\nTWFu";
byte[] bytes = Convert.FromBase64String(attachmentBody);
Console.WriteLine(Encoding.ASCII.GetString(bytes));
// ManManMan
Per i payload che arrivano attraverso un flusso piuttosto che una stringa, FromBase64Transform con la sua modalità che ignora gli spazi bianchi è la stessa storia in abiti di streaming. E quando devi fare di più che decodificare il corpo, quando devi percorrere la struttura MIME, analizzare gli header, gestire sezioni multipart annidate, o estrarre ogni allegato da un file .eml vero, la risposta dell'ecosistema in C# è il pacchetto MimeKit: è la libreria MIME standard per .NET, gestisce internamente le codifiche di trasferimento del contenuto Base64 e quoted-printable, ed è lo strumento da afferrare nel momento in cui "decodifica solo il corpo" smette di descrivere il tuo problema. La classe MailMessage del framework stesso decodificherà gli allegati semplici per te, ma il suo supporto MIME è deliberatamente modesto secondo gli standard moderni.
Certificati PEM
Il PEM è il formato corazzato del mondo TLS: un corpo Base64 tra i marker -----BEGIN CERTIFICATE----- e -----END CERTIFICATE-----, avvolto a 64 caratteri, come specificato dalla RFC 7468. Gli sviluppatori C# lo incontrano come i file di certificato dietro ogni endpoint HTTPS, e la storia della decodifica qui è meglio di quanto potresti aspettarti, perché da .NET 6 il framework analizza il PEM per te, corpo Base64 e tutto il resto:
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
Nessun Base64 manuale da nessuna parte: CreateFromPem trova i marker, sveste il corpo, lo decodifica e ti restituisce un certificato vivo. (La famiglia ha fratelli per le chiavi private e per la forma combinata certificato-più-chiave, nel caso la tua infrastruttura te le passi.) Se sei su un runtime più vecchio, o ti servono i byte DER grezzi che stanno dentro l'armatura, la versione manuale è uno strip-and-decode in due passi, e vale la pena conoscerla perché lo stesso pattern funziona per qualunque cosa corazzata in 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 lunghezza del certificato DER dentro l'armatura
Le insidie di questo angolo sono tutte spazi bianchi: i file PEM portano fine riga CRLF dalla maggior parte degli strumenti di certificato, quindi togli sia \r che \n prima di decodificare, non solo gli a capo. E non confondere il corpo del certificato con il corpo di una chiave privata, che ha marker diversi e contenuti diversi; un decoder non ti salverà da quella.
Configurazione, variabili d'ambiente e database
La terza casa del Base64 nelle applicazioni C# è lo storage: file di configurazione, variabili d'ambiente e colonne di database. Il pattern è lo stesso ovunque. Un valore binario o segreto viene codificato in una stringa in entrata, e decodificato di nuovo in byte in uscita. Le variabili d'ambiente sono l'esempio più visibile, perché possono contenere solo testo:
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.");
In un database la stessa idea di solito compare come una proprietà byte[] che vuoi immagazzinare in una colonna di testo per portabilità, e Entity Framework Core ha un meccanismo built-in esattamente per questo: un convertitore di valori che esegue in modo trasparente le tue funzioni di codifica e decodifica a ogni lettura e scrittura:
using Microsoft.EntityFrameworkCore;
modelBuilder.Entity<Avatar>()
.Property(a => a.ImageData)
.HasConversion(
v => Convert.ToBase64String(v),
v => Convert.FromBase64String(v));
Quel singolo converter è l'intera integrazione col database: ImageData resta un byte[] nel tuo codice C#, e il database vede una stringa Base64. Due avvertenze spettano a questa sezione. Prima, una colonna di una data larghezza contiene circa un terzo di dati in meno come testo codificato che come binario grezzo, a causa della tassa di 4 caratteri per 3 byte, quindi dimensiona la colonna per la lunghezza codificata se è a larghezza fissa. Secondo, ed è quella di sicurezza: il Base64 in un file di configurazione è una comodità per tenere un valore su una singola riga, non una protezione per il valore. Chiunque può leggere il file di configurazione può decodificare la chiave in un singolo comando, ed è per questo che i veri segreti stanno in un secret store, e il Base64 lì è solo il formato di trasporto.
Quando il payload è grande
La decodifica Base64 ha una proprietà gradevole che la codifica non ha: l'output è sempre più piccolo dell'input, circa tre quarti. Un payload di testo da 10 megabyte si decodifica in circa 7,5 megabyte di byte, quindi una decodifica non può mai gonfiare la memoria come può fare una codifica. L'aritmetica, se devi dimensionare un buffer in anticipo, si riduce a una di due chiamate: Base64.GetMaxDecodedFromUtf8Length per la classe span rigorosa, o la semplice divisione, length / 4 * 3 per l'API classica, più un margine per gli spazi bianchi se l'input è avvolto. (L'helper restituisce la lunghezza decodificata massima possibile: la lunghezza reale le è uguale solo quando l'ultimo gruppo non ha padding, ed è uno o due byte in meno quando termina con uno o due caratteri di padding.)
Quando il payload è davvero grande, però, la mossa giusta non è un buffer più grande - è nessun buffer affatto: salta la stringa completamente e lascia che FromBase64Transform faccia scorrere la decodifica dalla sorgente al target, come mostrato nella sezione sui flussi. L'unica regola da rispettare è l'allineamento al gruppo di quattro: un flusso Base64 può essere tagliato solo a multipli di quattro caratteri (dopo aver conteggiato gli spazi bianchi), quindi se mai alimenti la transform a mano, leggi in chunk che sono multipli di quattro e lascia che TransformFinalBlock dreni il resto. Per qualunque cosa sotto i centinaia di megabyte, la decodifica in un colpo solo è abbastanza veloce da renderla un'ottimizzazione, non una necessità, ma la forma in streaming è anche quella che si comporta bene sotto limiti di memoria, che sono esattamente gli ambienti in cui i payload grandi amano vivere.
Un decoder nel tuo terminale
C'è un momento soddisfacente, in ogni linguaggio, in cui un programma console di 15 righe diventa uno strumento a riga di comando, e il decoder Base64 di C# è un buon candidato per farlo, perché leggere dallo standard input lo rende un pronto all'uso per i pipe del shell. Ecco lo strumento intero: legge il payload Base64 dal pipe (o da un argomento), lo decodifica, e scrive i byte grezzi in un file:
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.");
Compilalo una volta, e sta accanto all'utilità base64 del shell stesso per i giorni in cui vuoi specificamente il decoder del runtime .NET: fai passare un file attraverso di esso, incatenalo con altri strumenti, e le regole di validazione rigorose di C# (tolleranti verso gli spazi bianchi, rigorose sull'alfabeto, rigorose sul padding) diventano parte del tuo pipeline. Il Trim() sta facendo lavoro silenzioso lì, catturando l'a capo finale che gli editor di testo amano aggiungere, anche se, per essere equi, il decoder l'avrebbe ignorato comunque. Per i payload URL-safe che compaiono sempre più spesso nei log delle API, lo stesso scheletro con la decodifica Base64Url della sezione URL-safe è l'intero cambiamento.
Velocità: cosa aspettarsi
Il Base64 nel .NET moderno è veloce, e continua a farsi più veloce. Le implementazioni runtime sia dei metodi Convert che delle classi System.Buffers.Text sono ottimizzate con istruzioni vettoriali SIMD dove l'hardware le supporta, e processano molti caratteri per ciclo. Nella pratica questo significa che i payload da diversi megabyte si decodificano in millisecondi a una cifra o a doppia cifra bassa su una macchina desktop ordinaria, abbastanza veloce da rendere la decodifica Base64 di fatto gratuita in qualsiasi applicazione che scriverai. Il consiglio pratico sulle performance riguarda quindi la forma del tuo codice, non il decoder in sé. Preferisci i metodi Try o i metodi span che restituiscono uno stato sui percorsi caldi, dove input malformati sono possibili e le eccezioni sarebbero costose. Riutilizza i buffer con le API in loco e span quando decodifichi migliaia di piccoli payload in un loop, invece di allocare un array fresco per chiamata. E non decodificare mai lo stesso payload due volte: una volta è il costo, e una seconda decodifica di un campo che hai già decodificato è puro spreco che compare nei profili come un misterioso secondo picco Base64.
Sicurezza: ciò che il Base64 non fa
Il fatto di sicurezza più importante sul Base64 è quello che i principianti più spesso perdono: è codifica, non cifratura. Una stringa Base64 è leggibile da chiunque, con qualunque strumento, in una frazione di secondo, e C# rende la lettura una riga sola, come questo intero articolo ha dimostrato. Il Base64 non ha chiave, non ha parametro d'algoritmo, e non ha debolezza da sfruttare, perché non ha mai provato a nascondere nulla: è un formato di trasporto, un modo per fare sopravvivere il binario in canali solo testo. Trattalo di conseguenza. Non mettere mai una password, un token o un segreto in un file di configurazione "protetto" dal Base64, perché la protezione è profonda esattamente una chiamata a Convert.FromBase64String. Se il valore deve essere segreto, ha bisogno di protezione vera (un secret manager, uno store cifrato, al minimo un controllo di accesso del sistema operativo), e il Base64 è solo la forma che indossa mentre viaggia.
La seconda nota di sicurezza riguarda il tuo percorso di decodifica. Ogni payload che decodifichi è un input non fidato fino a prova contraria, e i due modi di fallimento per cui progettare sono quello rumoroso (input invalido, a cui l'API classica risponde con una FormatException che dovresti catturare e convertire in un 400, non in un 500) e quello silenzioso (Base64 valido che si decodifica in byte che non sono quelli che ti aspettavi: non UTF-8, non il tipo di file che hai richiesto, o più lungo di quanto avevi previsto). Validare prima di fidarti: controlla la lunghezza con IsValid o la famiglia Try prima di allocare, controlla i byte decodificati contro una firma attesa (il numero magico PNG, l'header PKCS) prima di passarli a un parser di immagini o certificati, e dimensiona i tuoi buffer dalla lunghezza codificata prima di decodificare, non dopo. Il Base64 decodificherà qualunque cosa sia ben formata; decidere cosa ben formata significhi per la tua applicazione è il tuo lavoro.
Insidie da conoscere prima che mordano
Queste sono le trappole specifiche di C# che continuano a comparire nel codice vero, e ognuna ha una causa concreta nel modo in cui il framework funziona:
- Binario attraverso una stringa. Una
stringC# è una sequenza di unità di codice UTF-16, e il Base64 decodificato non lo è. Nel momento in cui infili byte decodificati in una variabile di tipo stringa (unConsole.WriteLinedi un PNG decodificato, una concat di stringhe con binario, una libreria JSON che serializza "testo"), qualcosa a valle lo stritolerà. Tieni il binario decodificato inbyte[]finché non raggiunge un posto che vuole davvero i byte. - La spaccatura di Encoding.Default. Un codice che legge byte decodificati con
Encoding.Defaultproduce testo diverso su .NET Framework (la code page ANSI di Windows) e su .NET (UTF-8). Lo stesso payload, due output diversi, nessuna eccezione. Fissa la tua codifica esplicitamente. - Segmenti JWT e decoder classico. Alimentare un segmento JWT grezzo a
Convert.FromBase64Stringfallisce in due modi allo stesso tempo: i caratteri-/_sono fuori dall'alfabeto standard, e il padding mancante rompe la regola di lunghezza. Normalizza prima, o usaBase64Url. - Spazi bianchi che vedi e spazi bianchi che non vedi. Il decoder salta spazio, tab, a capo e ritorno a capo, e non salta nient'altro. Uno spazio non separatore, un separatore di riga Unicode, o una tabulazione verticale in un payload (tutti cose che sopravvivono al copia-incolla da alcune pagine web) è una
FormatException, non una spalla da alzare. - Un messaggio d'errore per ogni crimine. La
FormatExceptiondel decoder classico non dice quale regola è rotta né dove. Fai il debug controllando la lunghezza, poi l'alfabeto, poi il padding, in quest'ordine, o passa aTryFromBase64StringeIsValidper una risposta booleana. - Sostituzione UTF-8 silenziosa.
Encoding.UTF8.GetStringtrasforma le sequenze di byte malformate in U+FFFD senza lamentarsi. Se il payload potrebbe non essere UTF-8 valido, usa il fallback rigoroso della sezione sui charset, o ti ritroverai a investigare dati mancanti settimane dopo che è successo. - Taglio del flusso nel posto sbagliato. Un flusso Base64 può essere tagliato solo a multipli di quattro caratteri. Fai i chunk di una decodifica in streaming su qualsiasi altro confine e l'ultimo gruppo parziale finisce in
TransformFinalBlock, dove o gli sta bene o rompe il tuo conteggio di allineamento. - Fine riga PEM. I file di certificato portano CRLF. Togli
\roltre che\nquando svesti l'armatura a mano, oppure la prima riga del tuo DER "decodificato" è un ritorno a capo vestito da byte di dati. - Doppia codifica. Se un payload era già Base64 quando ti è arrivato (una configurazione che ha fatto Base64 di una stringa Base64, un'API che ha codificato l'output di un altro encoder), una decodifica ti dà altro Base64, non i tuoi dati. Il giro completo si chiude solo dopo tante decodifiche quante erano le codifiche, e il lato encoder di quel bug è l'argomento dell'articolo sulla codifica.
Una breve storia del Base64 in C#
La storia del Base64 in C# è anche la storia del platform .NET che cresce, ed è più lunga di quanto la maggior parte della gente si aspetti:
- .NET Framework 1.1, aprile 2003. Arrivano
Convert.FromBase64Stringe i suoi fratelli, e portano il design che ancora definisce l'API: rigoroso sull'alfabeto, generoso sui quattro caratteri di spaziatura, diretto sui suoi errori. Per gran parte dei due decenni successivi questo singolo metodo è "il" decoder Base64 in C#. - .NET 2.0, 2005. L'enum
Base64FormattingOptionssi unisce aConvert, portando gli a capo in stile MIME sul lato codifica (e la corrispondente tolleranza sugli spazi bianchi sul lato decodifica, dove è già silenziosamente al lavoro). - .NET Core 2.1, 2018. L'era degli Span.
Convertottiene i metodiTrye una codifica basata sugli span, e la nuova classeSystem.Buffers.Text.Base64arriva con il suo contrattoOperationStatus, la decodifica in loco eIsValid, costruita per il mondo a zero allocazioni della riscrittura focalizzata sulla memoria. - .NET 5, 2020. Spediscono i fratelli hex (
Convert.ToHexStringe soci), lo stesso pattern di design del Base64 applicato a un alfabeto di 16 simboli, un segnale che il pattern della classe di conversione era diventato uno stile di casa. - .NET 6, 2021.
X509Certificate2.CreateFromPemrende il PEM un input di prima classe, e un'intera classe di codice per svestire l'armatura a mano diventa opzionale sui runtimes moderni. - .NET 9, novembre 2024.
System.Buffers.Text.Base64Urlentra finalmente in dotazione dopo anni di richieste della comunità, e il pacchettoMicrosoft.Bcl.Memorylo fa retroportare su .NET Framework 4.6.2 e successivi per i codebase legacy che fanno ancora girare tutto. - .NET 11, in preview al momento della scrittura. Il prossimo rilascio, atteso per la fine del 2026, aggiunge altre API di comodità Base64 e sovraccarichi ai tipi esistenti, continuando la lenta marcia verso una superficie più ergonomica.
Vale la pena tenerlo a mente: la codifica in sé è molto più vecchia di tutte queste cose. Il primo uso standardizzato di ciò che oggi chiamiamo MIME Base64 è stato il protocollo Privacy-Enhanced Mail nel 1987 (RFC 989), la MIME ha standardizzato la forma con a capo da 76 caratteri nel 1993, e la RFC 4648 nel 2006 ha dato al formato la sua specifica moderna, sensibile all'alfabeto, inclusa la variante URL-safe. Il C# ne ha ereditato tutto: ogni particolarità di a capo e di padding che incontri in un formato email di 30 anni fa è una particolarità che il decoder C# era progettato per assorbire.
Fatti curiosi del C#
- Il più piccolo smoke test.
"TWFu"si decodifica inMan. Tre byte, nessun padding, scuse zero. È l'hello world del debug Base64 in C#, ed esercita l'intero percorso felice in quattro caratteri. - Un decoder con un passato postale. La tolleranza sugli spazi bianchi non è un accidente di implementazione - è una decisione di design ereditata dal MIME: un intero corpo email avvolto a 76 caratteri, con tutte le sue coppie CRLF, è un singolo argomento valido per
Convert.FromBase64String. Il decoder è stato costruito per mangiare il formato che l'email usa da trent'anni. - Un errore, tre cause. Il messaggio classico di
FormatExceptionelenca tutti e tre i modi di fallimento che potrebbe stare riportando (carattere sbagliato, troppo padding, padding mal posizionato) e non dice quale si è verificato. È l'unico messaggio d'errore della superficie API che funziona come una domanda a risposta multipla. - Un namespace che mente un po'.
System.Buffers.Textsuona come se fosse di processing del testo, ma è in realtà la casa della conversione binario-verso-testo in generale:Utf8ParsereUtf8Formatter, che analizzano numeri e date direttamente in UTF-8, vivono proprio accanto alle classi Base64. - Il padding è facoltativo su un lato della famiglia. La classe
Base64UrldecodificaAQIDBA(sei caratteri, senza padding) eAQIDBA==(gli stessi byte con padding) negli stessi quattro byte, mentre il decoder classico accetta solo la forma con padding. Due decoder, due contratti, un runtime. - Stringhe che non dovrebbero esistere. Una stringa C# può legalmente contenere byte NUL, quindi
Encoding.UTF8.GetStringsu binario decodificato può produrre una "stringa" piena di caratteri di controllo che la console, il tuo writer CSV e metà delle librerie JSON del pianeta tratteranno ciascuno in modo diverso. Il sistema di tipi lo permette; l'ecosistema, per lo più, no. - Un relitto 1.1 in buone condizioni.
Convert.FromBase64CharArrayha la stessa firma a tre parametri da aprile 2003, sopravvivendo alla rivoluzione dei generics, alla rivoluzione degli Span e alla rivoluzione URL-safe senza un singolo sovraccarico aggiunto. L'era dei char-array in C# non è finita; sta solo riposando. - Undici caratteri, otto byte. Gli identificatori video di YouTube sono base64url senza padding: 11 caratteri che si decodificano in 8 byte.
Base64Url.GetMaxDecodedLength(11)ti dice l'8, e la decodifica è una riga sola, che è un bel modo per chiudere la giornata se sei il tipo di persona che scrive quel tipo di cose.
L'altra direzione
Questo è il lato decoder, ed è dove vive la maggior parte del dolore, perché la decodifica è il posto dove incontri i dati degli altri: le loro scelte di padding, i loro a capo, i loro alfabeti, i loro token. La direzione opposta, prendere i tuoi byte e impacchettarli nel Base64, è un problema più calmo con il suo insieme di decisioni da prendere e il suo insieme di trappole. La codifica Base64 in C#, dalla questione dei 76 caratteri ai token URL-safe, è coperta in profondità nell'articolo compagno linkato qui sotto, ed è una lettura breve e appagante una volta che sai cosa cercare.
Ultimo aggiornamento: 2026-09-08
Articolo correlato: Codifica Base64 in C# (CSharp): una guida completa