Você tem que lidar com o formato Base64? Então esse site é perfeito para você! Use o nosso ferramenta online super útil para codificar ou decodificar os seus dados.

Codificação Base64 em C# (CSharp): um guia completo

Você tem os bytes. Um PNG que precisa viajar dentro de uma resposta JSON, um token que tem que caber numa URL, uma linha de texto que está prestes a entrar num sistema que só aceita letras e dígitos. Em algum lugar entre a byte[] na sua mão e o canal que ela precisa atravessar, o C# oferece um menu de codificadores Base64, e escolher entre eles é a habilidade de verdade deste assunto. O one-liner clássico está no framework desde 2003, as opções baseadas em span e URL-safe chegaram com os runtimes modernos, e cada uma faz promessas diferentes sobre tamanho, quebras de linha e alfabeto. Este artigo passeia pelo menu inteiro, com exemplos funcionais para todo trabalho real que um codificador é chamado a fazer.

As regras da casa primeiro, num só fôlego, já que a página inicial deste site explica o formato em profundidade: o codificador pega a cada três bytes e escreve quatro caracteres de um alfabeto de 64 símbolos, preenchendo a cauda com um ou dois caracteres =, então os seus dados saem cerca de 33 por cento mais gordos do que chegaram. Esse número, e não qualquer código, é o fato mais importante deste artigo, e tudo abaixo é sobre pagá-lo com bom senso.

O menu de codificadores: escolha a sua ferramenta

Aqui está a família completa de APIs de codificação no mundo .NET, com a situação para a qual cada uma foi construída. Tudo está no próprio runtime, exceto a classe URL-safe nos frameworks mais antigos, que chega junto com um pequeno pacote NuGet:

API Disponível desde Para que serve
Convert.ToBase64String(byte[]) .NET Framework 1.1 (2003) O clássico. Array inteiro entra, string com padding sai. Sem opções, sem surpresas.
Convert.ToBase64String(byte[], int, int) .NET Framework 1.1 (2003) Codificar um recorte de um array maior, sem copiá-lo antes.
Convert.ToBase64String(byte[], Base64FormattingOptions) .NET 2.0 (2005) O clássico com um dial: opcionalmente insere uma quebra de linha a cada 76 caracteres, do jeito MIME.
Convert.ToBase64String(ReadOnlySpan<byte>, Base64FormattingOptions) .NET Core 2.1 (2018) A versão de span: codifica uma visão sobre um buffer, sem copiar o array, sem alocar um recorte.
Convert.ToBase64CharArray(byte[], int, int, char[], int) .NET Framework 1.1 (2003) Escreve num buffer de caracteres que você possui, e devolve quantos caracteres foram usados.
Convert.TryToBase64Chars(ReadOnlySpan<byte>, Span<char>, out int, ...) .NET Core 2.1 (2018) Booleano em vez de exceções: codifica se o buffer couber, relata false se não couber.
System.Buffers.Text.Base64.EncodeToUtf8, EncodeToUtf8InPlace .NET Core 2.1 (2018) A família estrita de span: códigos de status em vez de exceções, e inflação in-place para buffers que você já possui.
System.Buffers.Text.Base64Url.EncodeToString e irmãos .NET 9 (2024) O alfabeto URL-safe, emitido sem padding. No .NET Framework 4.6.2+ e no .NET Standard 2.0: o pacote NuGet Microsoft.Bcl.Memory.
ToBase64Transform + CryptoStream .NET Framework 1.1 (2003) Streaming: codifica um arquivo enquanto flui, sem segurar o payload inteiro em memória.

Se o seu projeto mira uma versão do .NET de 2018 em diante, as sete primeiras linhas e o par de streaming no final já estão na caixa. O Base64Url precisa do .NET 9 ou mais novo, ou do pacote Microsoft.Bcl.Memory em qualquer versão anterior. E um aviso para frente: as bibliotecas do .NET 11, em preview na data de escrita com lançamento geral esperado para o fim de 2026, adicionam mais APIs e sobrecargas de conveniência Base64 aos tipos existentes, então o menu continua crescendo. Nenhum outro pacote neste artigo é necessário.

A chamada padrão: Convert.ToBase64String

Noventa por cento da vida de codificação em C# é uma única chamada. Entregue os bytes a ele, e ele devolve a string que os carrega:

using System;
using System.Text;

string text = "Man";
byte[] bytes = Encoding.UTF8.GetBytes(text);
string packed = Convert.ToBase64String(bytes);
Console.WriteLine(packed);
// TWFu

Note a forma de dois passos, porque é a pergunta mais comum "por que o meu Base64 não bate" no C#. Não existe sobrecarga que receba uma string diretamente, e isso é por design: uma string do C# é UTF-16, e o framework se recusa a adivinhar quais bytes você quis quando disse "codifique este texto". Você escolhe a representação em bytes primeiro, com Encoding.UTF8.GetBytes (ou o charset que os dados realmente têm), e só então o passo do Base64 acontece. O resto da família clássica é a mesma chamada com a cintura mais apertada: a sobrecarga (byte[], int, int) codifica um recorte de um buffer sem copiar o recorte, e a sobrecarga de span faz o mesmo a partir de um ReadOnlySpan<byte>, que é a ferramenta certa quando os dados são uma janela sobre um buffer de leitura maior. Uma propriedade a mais do codificador clássico vale a pena dizer com calma: ele nunca falha e nunca pergunta. Ele sempre emite o alfabeto padrão, sempre inclui padding, e sempre te dá a mesma string para a mesma entrada, então uma string Base64 é uma impressão digital confiável dos bytes que a produziram.

A questão dos 76 caracteres: quebras de linha e Base64FormattingOptions

O codificador clássico tem um dial, e ele está lá desde o .NET 2.0: Base64FormattingOptions. Coloque em InsertLineBreaks e o codificador insere uma quebra de linha a cada 76 caracteres de saída, o comprimento de linha que a especificação MIME usa para anexos de e-mail. Coloque em None, ou use as sobrecargas sem a opção, e você recebe uma única string longa e ininterrupta:

using System;

byte[] bytes = new byte[90];
string plain = Convert.ToBase64String(bytes);
string wrapped = Convert.ToBase64String(bytes,
  Base64FormattingOptions.InsertLineBreaks);

Console.WriteLine(plain.Length);   // 120
Console.WriteLine(wrapped.Length); // 122, uma quebra de linha adicionada depois do caractere 76

Na prática, dois detalhes sobre esse dial importam. Primeiro, a quebra de linha que ele insere é o par do Windows, retorno de carro + quebra de linha, não uma quebra de linha solta. Ou seja, a saída embrulhada contém sequências \r\n, e qualquer código que depois "limpe" a string removendo apenas \n vai ficar com retornos de carro soltos escondidos nos dados. Segundo, a quebra acontece em 76 caracteres de saída codificada, e é por isso que o padrão MIME podia garantir que o transporte de e-mail, com seus limites de linha de 76 ou 78 caracteres, nunca dividiria um grupo de quatro caracteres Base64 entre linhas: 76 é múltiplo de quatro, então toda linha termina numa fronteira de grupo. Você quer a forma embrulhada quando está produzindo corpos de e-mail, blocos de texto estilo PEM, ou qualquer coisa que um pipeline de e-mail legado vai carregar. Quer a forma sem quebra em todo outro lugar: payloads JSON, tokens de URL, respostas de API e arquivos que serão decodificados por um parser estrito que desconfia de surpresas. E nunca quer a forma embrulhada dentro de um JWT, onde a especificação proíbe explicitamente quebras de linha, espaços em branco e até o padding.

Dono da saída: buffers de char e as APIs Try

Às vezes a string não é o objetivo, o buffer é. Você está fazendo append num array de caracteres de tamanho fixo, escrevendo num frame de protocolo, ou simplesmente não quer que o runtime aloque a saída para você. Para esses momentos, o codificador tem um modo de char buffer desde a época do 1.1, e um modo Try desde a era dos spans. O método de char buffer escreve num array que você fornece e te diz quantos caracteres ele usou, então dimensionar o buffer é seu trabalho, e a biblioteca padrão até te entrega a fórmula de dimensionamento:

using System.Buffers.Text;
using System.Text;

byte[] bytes = Encoding.ASCII.GetBytes("Man");
char[] buffer = new char[Base64.GetMaxEncodedToUtf8Length(bytes.Length)];
int written = Convert.ToBase64CharArray(bytes, 0, bytes.Length, buffer, 0);

string packed = new string(buffer, 0, written);
Console.WriteLine(packed);
// TWFu

O irmão Try faz o mesmo trabalho a partir de spans e responde com um booleano. Ele codifica a entrada no seu span de destino, relata a contagem de caracteres no parâmetro de saída, e retorna false se o destino era pequeno demais, sem escrever nada. Essa última propriedade é o que o torna seguro para usar com tamanhos de entrada não confiáveis: você nunca recebe um buffer meio cheio de uma chamada falha:

using System;

byte[] bytes = { 1, 2, 3 };
char[] buffer = new char[4];

if (Convert.TryToBase64Chars(bytes, buffer, out int written,
  Base64FormattingOptions.None))
{
  Console.WriteLine(new string(buffer, 0, written));
  // AQID
}
else
{
  Console.WriteLine("Buffer too small, nothing was written.");
}

Na família estrita de span em System.Buffers.Text.Base64, a mesma forma existe, mas com o contrato de OperationStatus em vez de booleano: EncodeToUtf8 enche um span de bytes que você possui e te diz, pelo status, se terminou, se ficou sem espaço ou se precisa de mais entrada, e EncodeToUtf8InPlace é a que você pega quando os dados binários já estão num buffer que você está disposto a crescer por dentro: codificar infla os dados, então o método escreve o texto Base64 por cima do final do mesmo buffer e relata qual é o comprimento do resultado. Todos eles compartilham uma regra sobre dimensionamento: a saída para n bytes de entrada é sempre 4 * ceil(n / 3) caracteres, incluindo o padding, e os helpers GetMaxEncodedToUtf8Length e Base64Url.GetEncodedLength implementam essa aritmética - o segundo para o comprimento sem padding, que é o tamanho com padding ou menor - então dimensione a partir dos helpers, nunca a partir de uma constante lembrada.

O codificador URL-safe: Base64Url

O alfabeto padrão tem dois caracteres que URLs não gostam. O + em uma query string é rotineiramente decodificado como espaço pelas regras de parsing de formulários, e tanto o / quanto o = pedem percent-encoding antes de poder andar num path ou parâmetro. A variante URL-safe do Base64, definida na seção 5 do RFC 4648, troca + e / por - e _, que não precisam de escape em lugar nenhum, e torna o padding final com = opcional. Desde o .NET 9 o runtime tem uma classe dedicada para isso, a System.Buffers.Text.Base64Url, e ela tem um comportamento que surpreende as pessoas na primeira vez: ela não emite padding nenhum:

using System.Buffers.Text;

byte[] bytes = { 1, 2 };
string classic = Convert.ToBase64String(bytes);
string urlSafe = Base64Url.EncodeToString(bytes);

Console.WriteLine(classic); // AQI=
Console.WriteLine(urlSafe); // AQI

Essa diferença é o ponto inteiro. Um segmento de JWT, um identificador de upload, um token numa query string, um valor num path de URL: todos eles querem a forma URL-safe sem padding, e o Base64Url.EncodeToString a entrega diretamente, com o alfabeto e o padding tratados do jeito que esses formatos especificam. A classe tem a família completa, codificar para string, para span de char e para span de bytes UTF-8, além do GetEncodedLength para dimensionar buffers e do IsValid para validar a entrada chegando. Se o seu projeto roda num runtime mais antigo, adicione o pacote Microsoft.Bcl.Memory, que a Microsoft publica para backportar a classe para o .NET Framework 4.6.2 e acima:

dotnet add package Microsoft.Bcl.Memory

E se você não puder usar o pacote, a versão artesanal é o codificador clássico mais dois replaces e um trim, que você vai encontrar em uma grande quantidade de codebases C#:

using System;
using System.Text;

byte[] bytes = Encoding.UTF8.GetBytes("Hello World!");
string packed = Convert.ToBase64String(bytes)
  .Replace('+', '-')
  .Replace('/', '_')
  .TrimEnd('=');

Console.WriteLine(packed);
// SGVsbG8gV29ybGQh, URL-safe e sem padding

A ordem das operações nessa cadeia merece nota: as trocas de caracteres acontecem sobre a saída padrão, e o padding é cortado por último, porque cortar primeiro não mudaria nada mas tornaria o código mais difícil de ler, e trocar depois de cortar ainda funcionaria, mas é assim que bugs sutis nascem. Use essa forma para tokens, identificadores e qualquer coisa que vai morar numa URL, e reserve o alfabeto padrão para corpos de e-mail, payloads JSON e arquivos, onde +, / e = estão perfeitamente em casa.

Alimentando o codificador: strings, charsets e a escolha da encoding

Todo trabalho de codificação que parte de texto começa com a mesma decisão silenciosa: em quais bytes este texto vai se transformar? O passo do Base64 é determinístico e inocente, mas o passo da Encoding antes dele é onde as saídas divergem, e a divergência pode ser silenciosa. O UTF-8 é a suposição padrão na web moderna, e é o padrão certo aqui: ele faz ida e volta com todo idioma, é o que toda outra plataforma vai assumir ao decodificar o seu payload, e é o que Encoding.UTF8 te dá em uma chamada:

using System;
using System.Text;

string original = "h\u00e9llo \u4e16\u754c";
byte[] utf8 = Encoding.UTF8.GetBytes(original);
string packed = Convert.ToBase64String(utf8);
Console.WriteLine(packed);
// aMOpbGxvIOS4lueVjA==

Agora veja o mesmo caractere codificado por um charset diferente, e entenda por que "o mesmo texto" não é uma coisa bem definida sem um charset anexado:

using System;
using System.Text;

string euro = "\u20ac";
string asUtf8 = Convert.ToBase64String(Encoding.UTF8.GetBytes(euro));
string asLatin1 = Convert.ToBase64String(
  Encoding.GetEncoding("ISO-8859-1").GetBytes(euro));

Console.WriteLine(asUtf8);   // 4oKs
Console.WriteLine(asLatin1); // Pw==

O mesmo símbolo de euro produz duas strings Base64 diferentes, ambas perfeitamente válidas, e apenas uma delas vai decodificar de volta para um símbolo de euro do outro lado. A armadilha com o maior raio de explosão é o Encoding.Default: no .NET Framework no Windows é a code page ANSI do sistema, enquanto no .NET (Core) é UTF-8, então um programa que codifica com Encoding.Default produz Base64 diferente numa máquina de 2010 do que numa de 2025, e as duas saídas decodificam "corretamente" na sua plataforma de origem. Se um payload decodificado chega cheio de mojibake acentuado, a codificação original usou um charset diferente do que o decode assumiu, e a correção é deste lado do cano: fixe a encoding explicitamente, nas duas direções, em código que vai sobreviver à equipe que o escreveu. E uma nota final sobre o próprio sistema de tipos: uma string do C# é UTF-16, então se algum dia você passar unidades de código UTF-16 cruas para o codificador (chamando Encoding.Unicode.GetBytes), cada caractere ASCII custa dois bytes e a sua saída dobra de tamanho sem nenhum benefício, porque o decodificador do outro lado vai ler como texto UTF-16, não como os bytes da sua string original. O Base64 carrega os bytes que você dá a ele, e não liga para o que eles significam.

Arquivos: do disco para uma string

Arquivos são o payload de codificação mais comum e o mais tolerante, porque não existe a questão do charset: os bytes no disco são os dados, e o codificador não liga se eles formam uma palavra ou uma forma de onda. A ida e volta é uma leitura, uma codificação e uma gravação, e a única decisão de verdade é para onde o resultado vai:

using System.IO;

byte[] bytes = File.ReadAllBytes("photo.png");
string packed = Convert.ToBase64String(bytes);
File.WriteAllText("photo.b64", packed);

Console.WriteLine(packed.Length + " characters for "
  + bytes.Length + " bytes of image.");

A matemática do tamanho é a história inteira, e vale a pena fazer antes de escolher o transporte. Um megabyte de arquivo vira 1.333.336 caracteres Base64, e como uma string do C# guarda dois bytes por caractere, esse resultado codificado ocupa cerca de 2,7 megabytes em memória como string. Um arquivo de dez megabytes vira uma string de treze megabytes morando em 26 megabytes de memória gerenciada. Nada disso é problema para uma foto ou um blob de configuração, e é uma razão muito boa para usar o codificador em streaming, abaixo, quando o payload é um vídeo. O padrão acima é o que se usa para qualquer coisa que caia confortavelmente em memória, e também é o padrão que todo recurso "enviar um arquivo como Base64 num corpo JSON" usa em silêncio: leia o arquivo, codifique, coloque a string no JSON e deixe a camada de API fazer o trabalho dela.

Imagens na web: montando data URIs

O consumidor mais visível de imagens codificadas é a web, e o formato da web para "uma imagem que vive dentro do documento" é o data URI: um scheme data: seguido do tipo MIME, da flag ;base64, de uma vírgula e dos bytes codificados. Montar um em C# é concatenação de string, e o codificador está fazendo todo o trabalho de verdade:

using System.IO;

byte[] png = File.ReadAllBytes("logo.png");
string packed = Convert.ToBase64String(png);
string dataUri = "data:image/png;base64," + packed;

Console.WriteLine(dataUri.Substring(0, 30));
// data:image/png;base64,iVBORw0K

O prefixo iVBORw0KGgo naquela saída é um ponto de verificação útil: é a forma Base64 da assinatura PNG de oito bytes, então qualquer PNG que você codificar vai começar assim, e qualquer data URI de PNG que não comece assim não é um PNG. Três notas práticas pertencem a este padrão. Primeiro, o data URI é uma cópia completa da imagem, inflada em um terço, embutida no seu HTML ou CSS, então ele troca uma requisição de rede por peso permanente na página, um bom negócio para um favicon de 4 KB e um crime para uma hero image de 4 MB, e o codificador não vai negociar os 33 por cento. Segundo, se a imagem for grande, redimensione ou recomprima-a antes de codificar, porque cada byte do original aparece na página. Terceiro, tenha cuidado com SVG fornecido por usuário em HTML voltado para usuários: um SVG pode carregar script, então embuti-lo - inline, ou via <object>/<embed> - é uma superfície clássica de XSS. Um data URI em <img> simples não vai executá-lo em navegadores modernos, mas a mesma marcação reutilizada nesses contextos vai. PNG, JPEG, GIF e WebP em data URIs são inócuos; o SVG é o que não é.

Montando um JWT à mão

Montar um JSON Web Token do zero é um rito de passagem, e é um rito melhor no C# do que na maioria das linguagens, porque as peças são curtas. Um JWT é três segmentos base64url unidos por pontos: o header codificado, o payload codificado e a assinatura. Os dois primeiros são documentos JSON UTF-8, e a assinatura é computada sobre os dois primeiros segmentos unidos por um ponto. Aqui está a montagem inteira, com uma assinatura substituta, porque o passo criptográfico pertence à sua chave de assinatura e não à história do Base64:

using System;
using System.Buffers.Text;
using System.Text;

string headerJson = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
string payloadJson = "{\"sub\":\"42\",\"name\":\"Ada\"}";

string header = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(headerJson));
string payload = Base64Url.EncodeToString(Encoding.UTF8.GetBytes(payloadJson));
string signature = "c2lnbmF0dXJl"; // substituto do valor real de HMAC ou ECDSA

string jwt = header + "." + payload + "." + signature;
Console.WriteLine(jwt);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsIm5hbWUiOiJBZGEifQ.c2lnbmF0dXJl

Duas propriedades do Base64Url.EncodeToString fazem trabalho discreto naquele exemplo. Ele emite o alfabeto URL-safe, então nem o + nem o / podem aparecer no token, e omite o padding, então nenhum = aparece também, que é exatamente o que a especificação JWS exige, e exatamente o que o Convert.ToBase64String não faria sem ajuda. Se você está num runtime anterior ao .NET 9, o mesmo trabalho roda através do codificador padrão mais a cadeia de correção da seção URL-safe: codifique, troque os dois caracteres, corte o padding. A ordem dos segmentos importa para a assinatura, que é computada sobre header mais um ponto mais payload como bytes ASCII crus, então monte os dois segmentos primeiro e assine a concatenação exata deles, não uma versão reformulada do JSON. E um limite para manter afiado: para qualquer coisa que um usuário pode alcançar, não monte JWTs à mão de jeito nenhum. O pacote System.IdentityModel.Tokens.Jwt cuida de montagem, assinatura, validação e expiração por você, e o tratamento de base64url dele é precisamente este alfabeto e esta regra de padding. Montagem à mão é para testes, demos e o dia em que você precisa entender exatamente o que a biblioteca está fazendo.

Headers HTTP: autenticação Basic

O Base64 aparece no HTTP puro no esquema de autenticação Basic, e o lado da codificação é um dos montadores de header mais curtos do protocolo: una o username e a senha com dois-pontos, codifique o resultado como UTF-8, faça o Base64 e coloque o nome do esquema como prefixo:

using System;
using System.Text;

string user = "ada";
string password = "s3cret";
string credentials = user + ":" + password;

string header = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(credentials));
Console.WriteLine(header);
// Basic YWRhOnMzY3JldA==

O charset é a parte chata: o RFC 7617 deixa o charset padrão do esquema Basic indefinido por compatibilidade reversa e oferece apenas uma dica consultiva de UTF-8, mas é exatamente isso que todo servidor moderno espera, então um username acentuado deve passar por Encoding.UTF8, e não por qualquer default da plataforma, ou o servidor vai decodificar uma string de bytes diferente e rejeitar o login. O passo do Base64 é a única codificação no header: não faça percent-encoding do resultado, não faça URL-encoding, não faça Base64 duplicado. Cada um desses passos extras "ajudadores" é um bug conhecido, e o de codificação dupla é o mais comum, porque as credenciais às vezes chegam pré-codificadas de uma camada que já fez Base64 delas, e um segundo encode produz um header que parece plausível e falha em silêncio no servidor. Duas cautelas sobre o próprio esquema, para que caiam aqui em vez da seção de segurança, onde seriam diluídas: a autenticação Basic transmite a senha numa forma que está a um comando de ser legível, então só é aceitável sobre TLS, e mesmo assim é a ferramenta errada para a maioria do trabalho com APIs, que é por isso que tokens bearer e JWTs assumiram o lugar. O trabalho do codificador em tudo isso é o pequeno e honesto: transformar as credenciais unidas por dois-pontos numa string segura para header, e nada mais.

E-mail: MIME e por que o ToBase64Transform não quebra

O e-mail é o lar histórico do Base64, e continua sendo o lugar de onde vem a regra da linha de 76 caracteres: a especificação MIME quebra os corpos codificados em 76 caracteres, com CRLF entre as linhas, para que nenhum salto SMTP tenha motivo para quebrá-las de novo. O C# te dá dois codificadores para este trabalho, e eles fazem promessas diferentes, o que vale a pena entender antes de escolher um. O primeiro é o clássico Convert.ToBase64String com InsertLineBreaks, que você viu na seção de quebras de linha, e ele é exatamente a forma MIME, quebrado em 76 com CRLF, pronto para colar sob um header Content-Transfer-Encoding: base64. O segundo é o ToBase64Transform, o primo em streaming, e aqui está a surpresa: ele não insere quebras de linha. Não tem modo para isso, não tem opção, não tem flag de construtor, e a saída é um único stream longo e sem quebras:

using System.IO;
using System.Security.Cryptography;

using FileStream source = File.OpenRead("photo.png");
using MemoryStream destination = new MemoryStream();
using ToBase64Transform transform = new ToBase64Transform();
using CryptoStream encoder = new CryptoStream(source, transform, CryptoStreamMode.Read);

encoder.CopyTo(destination);
Console.WriteLine(destination.Length + " characters, no line breaks");

Então a regra prática é: para payloads de e-mail pequenos a médios, leia os bytes e use o codificador clássico com quebra, porque você recebe a forma MIME diretamente. Para anexos grandes, use streaming com o ToBase64Transform para manter a memória plana, e quebre o resultado você mesmo se o transporte realmente precisar de linhas de 76 caracteres, dividindo a saída nas fronteiras de grupo (a cada 76 caracteres, que é sempre uma fronteira de grupo, como a seção de quebras de linha explicou). A transform está fazendo a coisa certa ao permanecer sem quebras: ela processa a entrada em grupos de três bytes, e as quebras de linha são uma decisão de formatação que pertence à camada que conhece o transporte, não à camada que está convertendo bytes em caracteres num cano.

Streaming: codificando arquivos grandes sem lê-los duas vezes

Quando o payload é um vídeo, um backup ou qualquer outra coisa em que você teria vergonha de segurar numa string, o codificador em streaming é a solução inteira. O padrão é o espelho do streaming do lado do decode: um CryptoStream sobre o arquivo de origem, com o ToBase64Transform em modo de leitura, e um CopyTo para o alvo. O arquivo flui para dentro, o Base64 flui para fora, e a única memória que o processo segura é o buffer que o stream usa internamente:

using System.IO;
using System.Security.Cryptography;

using FileStream source = File.OpenRead("video.mp4");
using FileStream target = File.Create("video.b64");
using ToBase64Transform transform = new ToBase64Transform();
using CryptoStream encoder = new CryptoStream(source, transform, CryptoStreamMode.Read);

encoder.CopyTo(target);
Console.WriteLine("Wrote " + target.Length + " characters.");

Dois fatos sobre este padrão valem a pena guardar. Primeiro, o tamanho da saída é determinado inteiramente pelo tamanho da entrada, 4 caracteres por 3 bytes, então você pode reservar o espaço do alvo, pré-computar o comprimento para um header content-length, ou orçar uma cota de disco antes que um único byte flua. Segundo, a transform espera a entrada em grupos de três bytes, e o CryptoStream cuida desse alinhamento por você, alimentando a transform exatamente com o que ela quer enquanto o arquivo flui. Se algum dia você for conduzir a transform à mão com TransformBlock, alimente em múltiplos de três, e deixe o TransformFinalBlock drenar a cauda, os um ou dois bytes restantes que viram o grupo parcial final com seus um ou dois caracteres de padding. Para a maioria das aplicações a forma CopyTo é tudo o que você vai escrever, e é a forma que se comporta bem sob um limite de memória, que é exatamente onde os arquivos grandes gostam de morar.

Configuração, variáveis de ambiente e bancos de dados

Outro trabalho comum de codificação em aplicações C# é o trabalho de armazenamento: pegar um segredo ou um blob binário e colocá-lo num lugar que só aceita texto. Variáveis de ambiente são o exemplo visível, porque uma variável de ambiente é, por definição, uma string:

using System;
using System.Text;

string secret = "p@ssw0rd+and/symbols";
string packed = Convert.ToBase64String(Encoding.UTF8.GetBytes(secret));
Environment.SetEnvironmentVariable("SECRET_B64", packed);

string back = Encoding.UTF8.GetString(
  Convert.FromBase64String(Environment.GetEnvironmentVariable("SECRET_B64")));
Console.WriteLine(back == secret);
// True

Em bancos de dados, a mesma ideia geralmente aparece como uma propriedade byte[] que uma coluna de texto tem que guardar, e o Entity Framework Core tem um mecanismo embutido exatamente para isso, um conversor de valor que roda suas funções de encode e decode em cada leitura e escrita:

using Microsoft.EntityFrameworkCore;

modelBuilder.Entity<Avatar>()
  .Property(a => a.ImageData)
  .HasConversion(
    v => Convert.ToBase64String(v),
    v => Convert.FromBase64String(v));

Aquele conversor é a integração de banco inteira: o código C# vê uma byte[], a coluna vê uma string Base64, e a ida e volta é invisível no ponto de chamada. Duas cautelas pertencem a esta seção. Primeiro, a coluna está pagando o imposto de 33 por cento: uma coluna de texto dimensionada para o comprimento codificado guarda um terço a menos de dados do que a mesma largura em binário, então se você tem uma coluna de largura fixa, dimensione-a para o comprimento Base64, e se tem um varchar(max) ou equivalente, o imposto é só uma questão de cobrança. Segundo, e esta é a que volta sempre, Base64 num arquivo de configuração é uma forma, não um escudo. Ele mantém o valor em uma linha, mantém fora do caminho de editores de texto, e está a um comando de ser legível por qualquer um que possa ler o arquivo. Segredos precisam de proteção de verdade, um cofre de segredos, um vault de chaves, no mínimo permissões de arquivo, e o Base64 é apenas o formato de transporte que o segredo veste enquanto mora na configuração.

Da linha de comando

Todo codificador merece uma vida de console de 15 linhas, e o do C# é gostoso porque a saída é uma string comum para a qual a saída padrão foi feita. Aqui está a ferramenta inteira: ela recebe um caminho de arquivo ou a entrada padrão, codifica e grava o Base64 no terminal, onde qualquer pipeline do shell pode pegar:

using System;
using System.IO;
using System.Text;

string input = args.Length > 0
  ? File.ReadAllText(args[0])
  : Console.In.ReadToEnd();

byte[] bytes = Encoding.UTF8.GetBytes(input);
Console.WriteLine(Convert.ToBase64String(bytes));

Compile uma vez, e ela fica ao lado da utilidade base64 do próprio shell para os dias em que você quer especificamente o comportamento do codificador .NET: o mesmo alfabeto, o mesmo padding, e o tratamento UTF-8 do runtime C# para o que o pipe entregar. Para arquivos binários, o mesmo esqueleto com File.ReadAllBytes no lugar de File.ReadAllText é a mudança inteira, e a saída então descreve os bytes exatos do arquivo em vez da sua interpretação textual. A ferramenta também é uma boa sonda: passe um arquivo por ela, passe a saída de volta pelo decodificador do artigo de decodificação, e faça o diff dos dois arquivos, o que é um teste de ponta a ponta satisfatório de que os dois lados do pipe concordam em cada byte.

Padding, ou os iguais finais

Os caracteres = finais de uma string Base64 são a contabilidade do formato, e os codificadores do C# não concordam sobre eles, que é a origem de um bug de interoperabilidade específico e comum. O clássico Convert.ToBase64String sempre preenche com padding, porque o decodificador clássico com o qual ele se emparelha sempre espera. O Base64Url.EncodeToString nunca preenche, porque os consumidores URL-safe que ele mira, JWTs e APIs de token, sempre esperam a forma compacta. Quando a sua saída cruza para um mundo com a expectativa oposta, a correção é aritmética, e é a mesma aritmética que o artigo de decodificação mostrou para a direção inversa:

using System;

string padded = Convert.ToBase64String(new byte[] { 1, 2 });
Console.WriteLine(padded);           // AQI=
Console.WriteLine(padded.TrimEnd('=')); // AQI, o que um consumidor URL-safe quer

string compact = "AQI";
string restored = compact + new string('=', (4 - compact.Length % 4) % 4);
Console.WriteLine(restored);         // AQI=, o que um decodificador clássico quer

A fórmula (4 - length % 4) % 4 é o universo inteiro do padding: ela adiciona zero, um ou dois caracteres para o comprimento cair num múltiplo de quatro, e o módulo externo impede que uma entrada já com padding ganhe extras. Dois avisos sobre padding, porque é onde código bem-intencionado erra. Nunca trate o = como dados: ele não carrega informação, então codificar uma string que já contém padding como se fosse payload, ou fazer URL-encoding do = para %3D dentro de uma query string, são duas formas de produzir uma saída que parece certa e decodifica errado. E cuidado com a pequena família de payloads legados onde o padding foi escrito com um caractere diferente, um ponto em alguns sistemas mais antigos, em vez do = padrão: se um valor que você recebe usa um ponto onde você espera padding, normalize-o de volta para = antes de decodificar, ou passe-o pelo caminho URL-safe sem padding.

Quão rápido ele roda

A codificação Base64 no .NET moderno é rápida, e a parte interessante não é a história da CPU, é a história da memória. As implementações do runtime são otimizadas com instruções vetoriais SIMD onde o hardware suporta, e entradas de vários megabytes codificam em milissegundos de um dígito a dois dígitos baixos numa máquina desktop comum, rápido o suficiente para que o codificador seja efetivamente grátis em qualquer aplicação que você for escrever. O conselho de desempenho que de fato muda código tem a ver com a forma. A saída é uma string do C#, e uma string do C# guarda dois bytes por caractere, então o custo em memória de um resultado codificado é de mais ou menos 2,7 bytes por byte de entrada (4 caracteres por 3 bytes de entrada, a 2 bytes por caractere), um número que vale a pena conhecer quando o payload está na casa dos megabytes. Se você está codificando milhares de payloads pequenos num loop, prefira as APIs de span e char buffer, que escrevem em buffers que você reutiliza, em vez das APIs de string, que alocam uma string gerenciada nova por chamada. Se está codificando um arquivo grande, pule a string por completo e use a transform em streaming, porque o custo de 2 bytes por caractere de segurar uma string de 13 megabytes é desperdício puro quando um CopyTo teria mantido o conjunto de trabalho em buffers de stream. E se você está produzindo saída embrulhada em MIME, lembre que o passo de embrulho é uma segunda passada sobre os dados, então embrulhe só quando o transporte precisa, não como padrão.

A conversa sobre segurança

O lado da codificação do Base64 tem uma lição de segurança, e é o inverso do lado do decodificador: você é quem faz a escolha de expor dados legíveis, e o formato não vai te impedir. Base64 é codificação, não criptografia. Não tem chave, não tem algoritmo, não tem sigilo de espécie nenhuma, e a saída da sua chamada ToBase64String está a um comando da entrada, em qualquer máquina, em qualquer linguagem, por qualquer pessoa. Então a primeira regra é sobre o que você escolhe codificar: nunca coloque uma senha, um token ou um segredo num arquivo de configuração "protegido" por Base64, porque a proteção é de exatamente uma chamada de decode de profundidade, e a pessoa lendo a configuração tem o comando. Se o valor precisa ser secreto, ele precisa de proteção de verdade, e o Base64 é apenas a forma que ele veste enquanto mora no campo de texto.

A segunda lição é sobre o canal, e é específica para as coisas que este artigo monta. Um header de autenticação Basic carrega a senha numa forma que qualquer proxy, qualquer log e qualquer middlebox pode ler, que é por isso que o esquema só é aceitável sobre TLS e está majoritariamente obsoleto fora de integrações legadas. Um data URI num HTML carrega a imagem, e se a imagem for um SVG fornecido por usuário, ele carrega o que o SVG carrega, que é por isso que o caso de SVG em data URI precisa do mesmo cuidado que qualquer conteúdo de usuário. E um valor Base64 numa URL está, literalmente, na URL, o que significa que está no histórico do navegador, no log de acesso do servidor, no header referrer e no cache do proxy, então tokens que precisam permanecer privados não pertencem em query strings, com padding ou sem. O codificador está fazendo o seu trabalho honesto nos três casos, transformando bytes numa string segura para carregar. A segurança está no que você carrega, e onde, e o formato é um mensageiro melhor que a maioria, mas é um mensageiro, não um cofre.

Armadilhas em que codificadores C# caem

Essas são as armadilhas que continuam aparecendo no lado da codificação de código C#, e todas elas têm uma causa concreta em como o framework funciona:

  • O charset que você não escolheu. Codificar uma string com Encoding.Default produz Base64 diferente no .NET Framework (a code page ANSI do Windows) e no .NET (UTF-8). As saídas são ambas válidas, ambas decodificam "corretamente" na plataforma de origem, e não são os mesmos bytes. Fixe a encoding explicitamente.
  • Codificação dupla. A entrada já era Base64 (uma configuração que codificou um valor já codificado, uma API que re-codifica a entrada), e o codificador, fazendo exatamente o que mandaram, produziu Base64 de Base64. O resultado parece plausível, e decodifica uma camada por vez, que é como um bug que leva dois decodes para corrigir acaba sendo descoberto em produção.
  • Quebras de linha no lugar errado. A forma embrulhada em MIME, com seus pares CRLF, cai numa string JSON, num segmento de JWT ou num parâmetro de URL, onde o consumidor estrito trava nos espaços em branco que nunca foi avisado a esperar. Embrulhe para o e-mail, deixe quieto em todo outro lugar, e se você estiver removendo o embrulho de outra pessoa, remova o \r também junto com o \n.
  • O alfabeto padrão numa URL. Um + numa query string é decodificado como espaço pelas regras de parsing de formulários, então um valor Base64 padrão colocado numa URL volta com letras nos lugares onde estavam os sinais de mais. Use o alfabeto URL-safe, ou faça percent-encoding do valor inteiro, e nunca os dois.
  • O descompasso do padding. A sua saída tem padding e o consumidor quer compacto, ou o contrário, e nenhum dos lados está errado - eles apenas discordam. A correção é a aritmética da seção de padding, aplicada no lado que conhece a expectativa do consumidor, que geralmente é o lado que escreve o token.
  • Memória que não foi orçada. A string codificada ocupa dois bytes por caractere em memória, então um arquivo de 10 MB vira uma string de 13 milhões de caracteres que pesa cerca de 27 MB em memória gerenciada, e um loop que monta strings assim uma por vez aparece no profiler como churn de alocação sem causa visível. Dimensione buffers com os helpers de comprimento, use streaming para os grandes, reutilize buffers nos loops quentes.
  • A transform que não quebra. O ToBase64Transform emite uma única linha longa. Código que faz streaming de um anexo "pronto para MIME" por ele e depois envia por e-mail produz uma linha de 120.000 caracteres que algum transporte vai quebrar no meio de um grupo, que é exatamente a corrupção para a qual a regra dos 76 caracteres foi desenhada.
  • Codificar a codificação. Passar uma string Base64 para o codificador porque "os dados já são texto" produz uma segunda camada. O codificador não sabe, e não liga, que a entrada parece Base64; ele codifica quantos caracteres a string acontece de ter, e o decodificador do outro lado recebe uma string Base64 no lugar onde esperava os seus dados.

Como o codificador cresceu: um tour por versões

O lado da codificação da API tem a própria linha do tempo, correndo da segunda release do .NET até a que está em preview agora:

  • .NET Framework 1.1, abril de 2003. Convert.ToBase64String e ToBase64CharArray chegam, a família clássica inteira numa release só, e as sobrecargas de recorte já incluídas, que é um pequeno milagre de visão para uma API de 2003.
  • .NET 2.0, 2005. Base64FormattingOptions e o valor InsertLineBreaks entram para a família, trazendo a quebra de linha MIME para o framework e encerrando uma era de loops de Substring feitos à mão em código de e-mail.
  • .NET Core 2.1, 2018. A era do span. A Convert ganha a codificação baseada em span e o TryToBase64Chars, e a nova classe System.Buffers.Text.Base64 chega com seu contrato OperationStatus e a inflação in-place, construída para o mundo de alocação zero.
  • .NET 5, 2020. Os irmãos em hex (Convert.ToHexString e cia.) saem, o mesmo padrão de design aplicado a um alfabeto de 16 símbolos, e o padrão de classe de conversão vira o estilo da casa.
  • .NET 7, 2022. X509Certificate2.ExportCertificatePem faz o framework produzir PEM por você, marcadores de armadura, quebra em 64 caracteres e corpo Base64 inclusos, o que aposentou em silêncio uma classe de código manual de formatação de certificado.
  • .NET 9, novembro de 2024. System.Buffers.Text.Base64Url chega na caixa, depois de anos de pedidos da comunidade, o pacote Microsoft.Bcl.Memory o backporta para o .NET Framework 4.6.2 e acima, e junto vem o comportamento sem padding que o código de JWT vinha montando à mão o tempo todo.
  • .NET 11, em preview na data de escrita. A próxima release, esperada para o fim de 2026, adiciona mais APIs e sobrecargas de conveniência do Base64 aos tipos existentes, continuando a marcha em direção a uma superfície mais ergonômica.

O próprio formato tem uma biografia mais antiga, e é por isso que a API do C# tem a cara que tem. O primeiro uso padronizado do que hoje chamamos de MIME Base64 foi o protocolo Privacy-Enhanced Mail em 1987 (RFC 989), a especificação MIME fixou a forma quebrada em linhas de 76 caracteres em 1993, e o RFC 4648 em 2006 deu ao formato a especificação moderna, consciente do alfabeto, incluindo a variante URL-safe, que só ganhou um codificador de primeira classe no C# em 2024. Três décadas de convenções de e-mail e web é a razão pela qual as quebras de linha, o padding e os dois alfabetos existem todos, e o codificador do C# é o lugar onde os três se encontram.

Pequenas maravilhas

  • O mínimo de quatro caracteres. A saída Base64 não vazia mais pequena possível é de quatro caracteres, porque o formato pensa em grupos de quatro mesmo quando você dá um byte. Um byte de qualquer coisa codifica em duas letras e dois sinais =, e essa forma, dois caracteres de dados vestindo um chapéu de padding, é uma impressão digital que você vai começar a reconhecer em configurações e tokens.
  • Nuls são bem-vindos. O codificador não tem opinião sobre o que os bytes significam, então um buffer cheio de zeros codifica de bom grado numa parede de caracteres A, e um arquivo binário com seus bytes NUL intactos faz ida e volta sem perder um único. A ansiedade de que "strings não seguram binário" pertence ao lado das strings do sistema de tipos, não ao codificador, que nunca vê uma string.
  • Determinismo como recurso. Os mesmos bytes, as mesmas opções, sempre a mesma string. Sem timestamp, sem sal aleatório, sem variação, que é por isso que uma string Base64 faz um bom fingerprint de gambeta do conteúdo de um arquivo: dois arquivos com o mesmo Base64 são o mesmo arquivo, e o check é uma comparação de string.
  • Dois bytes por caractere, de graça. Uma string do C# é UTF-16, então cada caractere da sua saída Base64 ocupa dois bytes em memória gerenciada. O codificador não anuncia isso, a propriedade de comprimento não relata, e uma string de 13 milhões de caracteres simplesmente pesa 26 MB, que é o número para ter na cabeça quando o payload é grande.
  • CRLF por herança. O embrulho MIME insere pares de retorno de carro e quebra de linha mesmo quando o seu código roda no Linux, porque a regra vem da especificação de e-mail, não da plataforma. O codificador é historiador tanto quanto conversor, e preserva os finais de linha de 1993 numa máquina de 2026.
  • Sobrecarga de recorte desde o primeiro dia. ToBase64String(byte[], int, int) codifica uma janela sobre um array maior desde 2003, quinze anos antes que os spans fizessem a ideia ficar na moda. Os designers de API da era 1.1 olharam buffers de verdade e adicionaram a forma offset-e-comprimento, e ela ainda é a escolha certa quando os dados são uma seção de uma leitura maior.
  • A linha de certificado de 64 caracteres. O PEM quebra em 64 caracteres, não 76, e o ExportCertificatePem sabe disso e quebra de acordo, que é um dos detalhes silenciosos que fazem "deixe o framework fazer" o conselho certo para trabalho com certificados. Dois comprimentos de quebra, uma família de formato, e o framework mantém os dois em ordem.
  • Dois alfabetos, dois nomes. Os 64 valores são chamados de "standard" numa parte da API e de "URL-safe" em outra, e diferem em exatamente dois caracteres: os slots 62 e 63. + e / de um lado, - e _ do outro, e todo bug de interoperabilidade deste artigo mora no momento em que alguém assumiu que os dois lados eram a mesma coisa.

Fechar o círculo

Esse é o lado do codificador, e é onde você faz as decisões: o alfabeto, o padding, as quebras de linha, o charset, o buffer. A outra direção, receber Base64 das outras pessoas, com as escolhas de padding deles, as quebras de linha deles, os alfabetos deles e os tokens deles, é onde mora a maior parte da dor, porque você não negocia com um payload. Decodificar Base64 em C#, do one-liner clássico às famílias de span e URL-safe, é coberto em profundidade no artigo companheiro linkado abaixo, e entre os dois o assunto inteiro cabe na sua memória de trabalho, que é o ponto de um formato tão antigo e tão pequeno.

Última atualização: 2026-09-08

Artigo relacionado: Decodificação Base64 em C# (CSharp): um guia completo