¿Tiene que ocuparse del formato Base64? Entonces esta página es perfecta para Ud. Utilice nuestra práctica herramienta en línea para codificar o decodificar sus datos.

Codificación Base64 en C# (CSharp): una guía completa

Tienes los bytes. Un PNG que necesita viajar dentro de una respuesta JSON, un token que tiene que caber en una URL, una línea de texto a punto de entrar en un sistema que solo acepta letras y dígitos. En algún punto entre el byte[] que tienes en la mano y el canal que debe cruzar, C# ofrece un menú de codificadores Base64, y elegir entre ellos es la habilidad real de esta materia. El clásico de una línea lleva en el framework desde 2003, las opciones basadas en span y URL-safe llegaron con los runtimes modernos, y cada una hace promesas distintas sobre tamaño, saltos de línea y alfabeto. Este artículo recorre el menú completo, con ejemplos que funcionan para cada trabajo real que se le pide a un codificador.

Primero las reglas de la casa, de un solo aliento, porque la página de inicio de este sitio explica el formato a fondo: el codificador toma cada tres bytes y escribe cuatro caracteres de un alfabeto de 64 símbolos, rellenando el final con uno o dos caracteres =, así que tus datos salen aproximadamente un 33 por ciento más gordos de como llegaron. Ese número, y no ningún código, es el hecho más importante de este artículo, y todo lo de abajo va de pagarlo con cabeza.

El menú de codificadores: elige tu herramienta

Aquí está la familia completa de APIs de codificación en el mundo .NET, con la situación para la que cada una está hecha. Todo está en el propio runtime, excepto la clase URL-safe en los frameworks antiguos, que viaja dentro de un pequeño paquete NuGet:

API Disponible desde Para qué sirve
Convert.ToBase64String(byte[]) .NET Framework 1.1 (2003) El clásico. Entra el array entero, sale una cadena rellena. Sin opciones, sin sorpresas.
Convert.ToBase64String(byte[], int, int) .NET Framework 1.1 (2003) Codifica una porción de un array más grande, sin copiarla primero.
Convert.ToBase64String(byte[], Base64FormattingOptions) .NET 2.0 (2005) El clásico con un dial: opcionalmente inserta un salto de línea cada 76 caracteres, a la manera MIME.
Convert.ToBase64String(ReadOnlySpan<byte>, Base64FormattingOptions) .NET Core 2.1 (2018) La versión span: codifica una vista de un buffer, sin copia de array, sin asignación de porción.
Convert.ToBase64CharArray(byte[], int, int, char[], int) .NET Framework 1.1 (2003) Escribe en un buffer de caracteres que es tuyo, y te devuelve cuántos caracteres se usaron.
Convert.TryToBase64Chars(ReadOnlySpan<byte>, Span<char>, out int, ...) .NET Core 2.1 (2018) Booleano en vez de excepciones: codifica si el buffer cabe, informa false si no cabe.
System.Buffers.Text.Base64.EncodeToUtf8, EncodeToUtf8InPlace .NET Core 2.1 (2018) La familia span estricta: códigos de estado en vez de excepciones, e inflación in-place para buffers que ya son tuyos.
System.Buffers.Text.Base64Url.EncodeToString y sus hermanos .NET 9 (2024) El alfabeto URL-safe, emitido sin padding. En .NET Framework 4.6.2+ y .NET Standard 2.0: el paquete NuGet Microsoft.Bcl.Memory.
ToBase64Transform + CryptoStream .NET Framework 1.1 (2003) Streaming: codifica un archivo mientras fluye, sin mantener nunca el payload entero en memoria.

Si tu proyecto apunta a una versión de .NET de 2018 o posterior, las siete primeras filas y la pareja de streaming de abajo ya vienen en la caja. Base64Url necesita .NET 9 o más nuevo, o el paquete Microsoft.Bcl.Memory en cualquier cosa anterior. Y una nota hacia adelante: las librerías de .NET 11, en preview en el momento de escribir esto con una salida general prevista para finales de 2026, añaden más APIs y sobrecargas de conveniencia de Base64 a los tipos existentes, así que el menú sigue creciendo. Ningún otro paquete de este artículo es necesario.

La llamada estándar: Convert.ToBase64String

El noventa por ciento de la vida de codificación en C# es una sola llamada. Pásale bytes, y te devuelve la cadena que los lleva:

using System;
using System.Text;

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

Fíjate en la forma de dos pasos, porque es la pregunta "¿por qué no me cuadra mi Base64?" más común en C#. No hay ninguna sobrecarga que reciba una string directamente, y es por diseño: una cadena de C# es UTF-16, y el framework se niega a adivinar qué bytes querías decir cuando dices "codifica este texto". Primero eliges la representación en bytes, con Encoding.UTF8.GetBytes (o con el charset que los datos realmente sean), y solo entonces ocurre el paso de Base64. El resto de la familia clásica es la misma llamada con la cintura más estrecha: la sobrecarga (byte[], int, int) codifica una porción de un buffer sin copiar la porción fuera, y la sobrecarga span hace lo mismo desde un ReadOnlySpan<byte>, que es la herramienta correcta cuando los datos son una ventana a un buffer de lectura más grande. Otra propiedad del codificador clásico merece decirse a cara descubierta: nunca falla y nunca pregunta. Siempre emite el alfabeto estándar, siempre incluye padding, y siempre te da la misma cadena para la misma entrada, así que una cadena Base64 es una huella fiable de los bytes que la produjeron.

La pregunta de los 76 caracteres: saltos de línea y Base64FormattingOptions

El codificador clásico tiene un dial, y lleva ahí desde .NET 2.0: Base64FormattingOptions. Ponlo en InsertLineBreaks y el codificador inserta un salto de línea después de cada 76 caracteres de salida, la longitud de línea que usa la especificación MIME para adjuntos de email. Ponlo en None, o usa las sobrecargas sin la opción, y obtienes una sola cadena larga e ininterrumpida:

using System;

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

Console.WriteLine(plain.Length);   // 120
Console.WriteLine(wrapped.Length); // 122, un salto de línea añadido después del carácter 76

Dos detalles de ese dial importan en la práctica. Primero, el salto de línea que inserta es la pareja de Windows, retorno de carro más salto de línea, no un salto de línea suelto. Así que la salida envuelta contiene secuencias \r\n, y cualquier código que luego "limpie" la cadena quitando solo \n se quedará con retornos de carro sueltos escondidos en los datos. Segundo, el envoltorio ocurre a los 76 caracteres de salida codificada, que es por lo que el estándar MIME podía garantizar que el transporte de email, con sus límites de línea de 76 u 78 caracteres, nunca partiría un grupo de cuatro caracteres Base64 entre líneas: 76 es múltiplo de cuatro, así que cada línea termina en un límite de grupo. Quieres la forma envuelta cuando produces cuerpos de email, bloques de texto al estilo PEM, o cualquier cosa que un pipeline de correo legacy vaya a llevar. Quieres la forma sin envolver en todas partes: payloads JSON, tokens de URL, respuestas de API, y archivos que decodificará un parser estricto al que no le gustan las sorpresas. Y nunca quieres la forma envuelta dentro de un JWT, donde la especificación prohíbe explícitamente los saltos de línea, los espacios en blanco, e incluso el padding.

Poseer la salida: buffers de caracteres y las API Try

A veces la cadena no es la meta, el buffer sí. Estás añadiendo a un array de caracteres de tamaño fijo, estás escribiendo en un frame de protocolo, o simplemente no quieres que el runtime asigne la salida por ti. Para esos momentos el codificador ha tenido un modo de buffer de char desde los días de la 1.1, y un modo Try desde la era de los span. El método de buffer de char escribe en un array que tú proporcionas y te dice cuántos caracteres usó, así que dimensionar el buffer es tu trabajo, y la biblioteca estándar hasta te da la fórmula de dimensionado:

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

El hermano Try hace el mismo trabajo desde spans y responde con un booleano. Codifica la entrada en tu span de destino, informa el recuento de caracteres en el parámetro de salida, y devuelve false si el destino era demasiado pequeño, sin escribir nada. Esa última propiedad lo hace seguro para usar con tamaños de entrada no confiables: nunca recibes un buffer a medio llenar de una llamada fallida:

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.");
}

Para la familia span estricta en System.Buffers.Text.Base64, existe la misma forma con el contrato de OperationStatus en vez de un booleano: EncodeToUtf8 rellena un span de bytes que es tuyo y te dice, por estado, si terminó, se quedó sin espacio o necesita más entrada, y EncodeToUtf8InPlace es la que usas cuando los datos binarios ya están en un buffer que estás dispuesto a hacer crecer: codificar infla los datos, así que el método escribe el texto Base64 por encima del final del mismo buffer e informa de lo largo que es el resultado. Todas estas comparten una regla sobre dimensionado: la salida para n bytes de entrada siempre son 4 * ceil(n / 3) caracteres incluyendo el padding, y los helpers GetMaxEncodedToUtf8Length y Base64Url.GetEncodedLength implementan esa aritmética - el segundo para la longitud sin padding, que siempre es el tamaño relleno o menos - así que dimensiona desde los helpers y nunca desde una constante memorizada.

El codificador URL-safe: Base64Url

El alfabeto estándar tiene dos caracteres que las URLs no quieren. El + en una cadena de consulta se decodifica rutinariamente como espacio por las reglas de análisis de formularios, y tanto / como = piden codificación por porcentaje antes de poder viajar en una ruta o un parámetro. La variante URL-safe de Base64, definida en la sección 5 del RFC 4648, intercambia + y / por - y _, que no necesitan escapado en ningún sitio, y hace opcional el padding final de =. Desde .NET 9 el runtime tiene una clase dedicada para ella, System.Buffers.Text.Base64Url, y tiene un comportamiento que sorprende la primera vez: no emite padding en absoluto:

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

Esa diferencia es todo el punto. Un segmento de JWT, un identificador de subida, un token en una cadena de consulta, un valor en una ruta de URL: todos quieren la forma URL-safe sin padding, y Base64Url.EncodeToString la da directamente, con el alfabeto y el padding manejados como esos formatos especifican. La clase tiene la familia completa: codificar a cadena, a span de char y a span de bytes UTF-8, más GetEncodedLength para dimensionar buffers y IsValid para validar la entrada de camino. Si tu proyecto corre en un runtime más antiguo, añade el paquete Microsoft.Bcl.Memory, que Microsoft publica para retroportar la clase a .NET Framework 4.6.2 y superior:

dotnet add package Microsoft.Bcl.Memory

Y si no puedes usar el paquete, la versión hecha a mano es el codificador clásico más dos replaces y un trim, que encontrarás en un montón de bases de código 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 y sin padding

El orden de operaciones en esa cadena merece una nota: los intercambios de caracteres ocurren sobre la salida estándar, y el padding se recorta al final, porque recortar primero no cambiaría nada pero haría el código más difícil de leer, e intercambiar después de recortar seguiría funcionando pero así es como nacen los bugs sutiles. Usa esta forma para tokens, identificadores, y cualquier cosa que vaya a vivir en una URL, y reserva el alfabeto estándar para cuerpos de email, payloads JSON y archivos, donde +, / y = están perfectamente en su elemento.

Alimentar el codificador: cadenas, charsets y la elección de codificación

Cada trabajo de codificación que empieza desde texto comienza con la misma decisión callada: ¿qué bytes se vuelve este texto? El paso de Base64 es determinista e inocente, pero el paso de Encoding que va antes es donde las salidas se divergen, y la divergencia puede ser silenciosa. UTF-8 es la suposición por defecto en la web moderna, y es el predeterminado correcto aquí: hace el viaje de ida y vuelta con cada lenguaje, es lo que toda otra plataforma asumirá cuando decodifique tu payload, y es lo que Encoding.UTF8 te da en una sola llamada:

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==

Ahora mira el mismo carácter codificado a través de un charset distinto, y entiende por qué "el mismo texto" no es algo bien definido sin un charset adjunto:

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==

Dos cadenas Base64 distintas para el mismo símbolo de euro, ambas perfectamente válidas, y solo una de ellas decodificará de vuelta a un símbolo de euro en el otro lado. La trampa con el radio de explosión más amplio es Encoding.Default: en .NET Framework en Windows es la página de código ANSI del sistema, mientras que en .NET (Core) es UTF-8, así que un programa que codifica con Encoding.Default produce Base64 distinto en una máquina de 2010 que en una de 2025, y ambas salidas decodificarán "correctamente" en su plataforma de origen. Si un payload decodificado llega lleno de mojibake con acentos, la codificación original usó un charset distinto al que la decodificación supuso, y la corrección está en este lado de la tubería: fija la codificación explícitamente, en las dos direcciones, en código que sobrevivirá al equipo que lo escribió. Y una nota final sobre el propio sistema de tipos: una cadena de C# es UTF-16, así que si algún día pasas unidades de código UTF-16 crudas al codificador (llamando a Encoding.Unicode.GetBytes), cada carácter ASCII cuesta dos bytes y tu salida duplica su tamaño sin beneficio, porque el decodificador del otro lado lo leerá como texto UTF-16, no como los bytes de tu cadena original. Base64 lleva los bytes que le das, y no le importa qué signifiquen.

Archivos: del disco a una cadena

Los archivos son el payload de codificación más común y el más indulgente, porque la pregunta del charset ni existe: los bytes en el disco son los datos, y al codificador le da igual si deletrean una palabra o una forma de onda. El viaje de ida y vuelta es una lectura, una codificación y una escritura, y la única decisión real es a dónde va el resultado:

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.");

La matemática de tamaño es toda la historia, y merece la pena hacerla antes de elegir un transporte. Un megabyte de archivo se vuelve 1.333.336 caracteres Base64, y como una cadena de C# almacena dos bytes por carácter, ese resultado codificado ocupa unos 2,7 megabytes en memoria como cadena. Un archivo de diez megabytes se vuelve una cadena de trece megabytes sentada en veintiséis megabytes de memoria gestionada. Nada de eso es un problema para una foto o un blob de configuración, y es una muy buena razón para usar el codificador en streaming, más abajo, cuando el payload es un video. El patrón de arriba es el que usas para cualquier cosa que cabe cómodamente en memoria, y es también el patrón que cada función de "subir un archivo como Base64 en el cuerpo JSON" usa en silencio: lee el archivo, codifícalo, mete la cadena en el JSON y deja que la capa de API haga su trabajo.

Imágenes en la web: construyendo data URIs

El consumidor más visible de imágenes codificadas es la web, y el formato de la web para "una imagen que vive dentro del documento" es la data URI: un esquema data: seguido del tipo MIME, una bandera ;base64, una coma, y los bytes codificados. Construir una en C# es concatenación de cadenas, y el codificador hace todo el trabajo de verdad:

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

El prefijo iVBORw0KGgo de esa salida es un punto de control útil: es la forma Base64 de la firma PNG de ocho bytes, así que cualquier PNG que codifiques empezará así, y cualquier data URI de PNG que no empiece así no es un PNG. Tres notas prácticas pertenecen a este patrón. Primera, la data URI es una copia completa de la imagen, inflada un tercio, incrustada en tu HTML o CSS, así que cambia una petición de red por peso permanente de página, un buen trato para un favicon de 4 KB y una estafa para una imagen principal de 4 MB, y el codificador no negocia el 33 por ciento. Segunda, si la imagen es grande, redimensiona o recomprímela antes de codificar, porque cada byte del original aparece en la página. Tercera, ten cuidado con los SVG que aporte el usuario en HTML de cara al usuario: un SVG puede llevar script, así que incrustarlo - en línea, o vía <object>/<embed> - es una superficie clásica de XSS. Una data URI de <img> simple no lo ejecutará en los navegadores modernos, pero el mismo markup reutilizado en esos contextos sí. PNG, JPEG, GIF y WebP en data URIs son inertes; SVG es el que no lo es.

Ensamblar un JWT a mano

Construir un JSON Web Token desde cero es un rito de paso, y en C# es un rito mejor que en la mayoría de los lenguajes, porque las piezas son cortas. Un JWT son tres segmentos base64url unidos por puntos: el encabezado codificado, el payload codificado y la firma. Los dos primeros son documentos JSON en UTF-8, y la firma se calcula sobre los dos primeros segmentos unidos por un punto. Aquí está el ensamblaje completo, con una firma sustituta, porque el paso criptográfico pertenece a tu clave de firma y no a la historia de 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"; // sustituto del valor HMAC o ECDSA real

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

Dos propiedades de Base64Url.EncodeToString hacen trabajo silencioso en ese ejemplo. Emite el alfabeto URL-safe, así que ni + ni / pueden aparecer en el token, y omite el padding, así que tampoco aparece ningún =, que es exactamente lo que exige la especificación JWS, y exactamente lo que Convert.ToBase64String no haría sin ayuda. Si estás en un runtime anterior a .NET 9, el mismo trabajo pasa por el codificador estándar más la cadena de arreglos de la sección URL-safe: codifica, intercambia los dos caracteres, recorta el padding. El orden de los segmentos importa para la firma, que se calcula sobre header más un punto más payload como bytes ASCII planos, así que ensamba primero los dos segmentos y firma su concatenación exacta, no una versión reformateada del JSON. Y un límite que conviene mantener nítido: para cualquier cosa a la que un usuario pueda llegar, no ensambles JWTs a mano en absoluto. El paquete System.IdentityModel.Tokens.Jwt maneja construcción, firma, validación y caducidad por ti, y su manejo de base64url es precisamente este alfabeto y esta regla de padding. El ensamblaje a mano es para pruebas, demos, y el día en que necesites entender exactamente qué está haciendo la librería.

Encabezados HTTP: autenticación Basic

Base64 aparece en HTTP plano en el esquema de autenticación Basic, y el lado de codificación es uno de los constructores de encabezados más cortos del protocolo: une el nombre de usuario y la contraseña con dos puntos, codifica el resultado como UTF-8, hazlo Base64, y ponle el nombre del esquema como prefijo:

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==

El charset es la parte traviesa: el RFC 7617 deja el charset predeterminado del esquema Basic sin definir por compatibilidad con versiones anteriores y solo ofrece una pista consultiva de UTF-8, pero eso es exactamente lo que todo servidor moderno espera, así que un nombre de usuario acentuado debería pasar por Encoding.UTF8, no por el predeterminado de la plataforma, o el servidor decodificará una cadena de bytes distinta y rechazará el inicio de sesión. El paso de Base64 es la única codificación del encabezado: no codifiques el resultado por porcentaje, no lo codifiques como URL, no lo hagas doble Base64. Cada uno de esos pasos extra "útiles" es un bug conocido, y el de doble codificación es el más común, porque las credenciales a veces llegan precodificadas desde una capa que ya les hizo Base64, y una segunda codificación produce un encabezado que parece plausible y falla en silencio en el servidor. Dos precauciones sobre el esquema en sí, para que caigan aquí y no en la sección de seguridad donde se diluirían: la autenticación Basic transmite la contraseña en una forma que está a un comando de ser legible, así que solo es aceptable sobre TLS, e incluso entonces es la herramienta equivocada para la mayoría del trabajo de API, y por eso los tokens bearer y los JWT tomaron el relevo. El trabajo del codificador en todo esto es el pequeño y honesto: convertir las credenciales unidas por dos puntos en una cadena segura para encabezados, y nada más.

Email: MIME y por qué ToBase64Transform no envuelve

El email es el hogar histórico de Base64, y sigue siendo el lugar de donde viene la regla de línea de 76 caracteres: la especificación MIME envuelve los cuerpos codificados a 76 caracteres con CRLF entre líneas, para que ningún salto SMTP tenga motivo de reenvolverlos. C# te da dos codificadores para este trabajo, y hacen promesas distintas, lo cual conviene entender antes de elegir uno. El primero es el clásico Convert.ToBase64String con InsertLineBreaks, que viste en la sección de saltos de línea, y es exactamente la forma MIME, envuelto a 76 con CRLF, listo para pegar bajo un encabezado Content-Transfer-Encoding: base64. El segundo es ToBase64Transform, el primo de streaming, y aquí viene la sorpresa: no inserta saltos de línea. No tiene modo para ellos, no tiene opción, no tiene bandera en el constructor, y su salida es una sola stream larga y sin envolver:

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");

Así que la regla práctica es: para payloads de email pequeños o medianos, lee los bytes y usa el codificador clásico que envuelve, porque obtienes la forma MIME directamente. Para adjuntos grandes, haz streaming con ToBase64Transform para mantener la memoria plana, y envuelve el resultado tú mismo si el transporte necesita de verdad líneas de 76 caracteres, separando la salida en límites de grupo (cada 76 caracteres, que siempre es un límite de grupo, como explicó la sección de saltos de línea). La transform hace lo correcto al quedarse sin envolver: procesa la entrada en grupos de tres bytes, y los saltos de línea son una decisión de formato que pertenece a la capa que conoce el transporte, no a la capa que está convirtiendo bytes en caracteres en una tubería.

Streaming: codificar archivos grandes sin leerlos dos veces

Cuando el payload es un video, un respaldo, o cualquier cosa que te daría vergüenza mantener en una cadena, el codificador en streaming es toda la solución. El patrón es el espejo del streaming del lado de decodificación: un CryptoStream sobre el archivo de origen, con ToBase64Transform en modo de lectura, y un CopyTo hacia el destino. El archivo fluye entrando, el Base64 fluye saliendo, y la única memoria que el proceso mantiene es el buffer que el 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.");

Dos hechos sobre este patrón merecen guardarse. Primero, el tamaño de la salida está totalmente determinado por el tamaño de la entrada, 4 caracteres por 3 bytes, así que puedes reservar el espacio del destino, precalcular la longitud para un encabezado content-length, o presupuestar una cuota de disco antes de que fluya un solo byte. Segundo, la transform espera su entrada en grupos de tres bytes, y CryptoStream maneja esa alineación por ti, alimentando a la transform exactamente lo que quiere mientras el archivo fluye. Si algún día pilotas la transform a mano con TransformBlock, aliméntala en múltiplos de tres, y deja que TransformFinalBlock drene el final, el uno o dos bytes sobrantes que se convierten en el último grupo parcial con su uno o dos caracteres de padding. Para la mayoría de las aplicaciones la forma CopyTo es todo lo que escribirás jamás, y es la forma que se comporta bien bajo un límite de memoria, que es exactamente donde a los archivos grandes les gusta vivir.

Configuración, variables de entorno y bases de datos

El otro trabajo de codificación común en aplicaciones C# es el de almacenamiento: tomar un secreto o un blob binario y meterlo en un lugar que solo acepta texto. Las variables de entorno son el ejemplo visible, porque una variable de entorno es, por definición, una cadena:

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

En las bases de datos la misma idea suele aparecer como una propiedad byte[] que una columna de texto debe sostener, y Entity Framework Core tiene un mecanismo incorporado para exactamente esto: un conversor de valores que ejecuta tus funciones de codificación y decodificación en cada lectura y escritura:

using Microsoft.EntityFrameworkCore;

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

Ese conversor es toda la integración con la base de datos: el código C# ve un byte[], la columna ve una cadena Base64, y el viaje de ida y vuelta es invisible en el punto de llamada. Dos precauciones pertenecen a esta sección. Primera, la columna está pagando el impuesto del 33 por ciento: una columna de texto dimensionada para la longitud codificada guarda un tercio menos de datos que el mismo ancho en binario, así que si tienes una columna de ancho fijo, dimensiona para la longitud Base64, y si tienes un varchar(max) o equivalente, el impuesto es solo un asunto de facturación. Segunda, y esta es la que vuelve una y otra vez, el Base64 en un archivo de configuración es una forma, no un escudo. Mantiene el valor en una línea, lo mantiene fuera del camino de los editores de texto, y está a un comando de ser legible para cualquiera que pueda leer el archivo. Los secretos necesitan protección real: un almacén de secretos, una bóveda de claves, como mínimo permisos de archivo, y el Base64 es solo el formato de transporte que el secreto viste mientras está sentado en la configuración.

Desde la línea de comandos

Cada codificador merece una vida de consola de 15 líneas, y el de C# es agradable porque la salida es una cadena plana para la que la salida estándar fue hecha. Aquí está la herramienta completa: toma una ruta de archivo o la entrada estándar, lo codifica, y escribe el Base64 en la terminal donde cualquier pipeline del shell puede tomarlo:

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));

Compílalo una vez, y queda junto a la propia utilidad base64 del shell para los días en que quieres específicamente el comportamiento del codificador .NET: el mismo alfabeto, el mismo padding, y el manejo UTF-8 del runtime C# de lo que la tubería le entregue. Para archivos binarios, el mismo esqueleto con File.ReadAllBytes en vez de File.ReadAllText es todo el cambio, y la salida entonces describe los bytes exactos del archivo en vez de su interpretación como texto. La herramienta es también una buena sonda: pasa un archivo a través de ella, pasa la salida de vuelta por el decodificador del artículo de decodificación, y haz diff de los dos archivos, que es una comprobación satisfactoria de punta a punta de que ambos lados de la tubería están de acuerdo en cada byte.

Padding, o los signos de igualdad finales

Los últimos caracteres = de una cadena Base64 son la contabilidad del formato, y los codificadores de C# no se ponen de acuerdo sobre ellos, que es la fuente de un bug de interoperabilidad específico y común. El clásico Convert.ToBase64String siempre rellena, porque el decodificador clásico con el que va siempre se lo espera. Base64Url.EncodeToString nunca rellena, porque los consumidores URL-safe a los que apunta, JWTs y APIs de tokens, siempre esperan la forma compacta. Cuando tu salida cruza a un mundo con la expectativa opuesta, la corrección es aritmética, y es la misma aritmética que el artículo de decodificación mostró para la dirección inversa:

using System;

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

string compact = "AQI";
string restored = compact + new string('=', (4 - compact.Length % 4) % 4);
Console.WriteLine(restored);         // AQI=, lo que un decodificador clásico quiere

La fórmula (4 - length % 4) % 4 es todo el universo del padding: añade cero, uno o dos caracteres para que la longitud caiga en un múltiplo de cuatro, y el módulo exterior impide que una entrada ya rellena gane extras. Dos advertencias sobre el padding, porque es donde el código bienintencionado se equivoca. Nunca trates el = como datos: no lleva información, así que codificar una cadena que ya contiene padding como si fuera payload, o codificar por porcentaje el = en %3D dentro de una cadena de consulta, son dos maneras de producir una salida que se ve bien y decodifica mal. Y cuida la pequeña familia de payloads legacy donde el padding se escribió como un carácter distinto, un punto en algunos sistemas más antiguos, en vez del = estándar: si un valor que recibes usa un punto donde esperas padding, normalízalo de vuelta a = antes de decodificar, o pásalo por la ruta URL-safe sin padding.

A qué velocidad corre

La codificación Base64 en .NET moderno es rápida, y la parte interesante es la historia de la memoria, no la del CPU. Las implementaciones del runtime están optimizadas con instrucciones vectoriales SIMD donde el hardware las soporta, y las entradas de varios megabytes codifican en un rango de 1 a 19 milisegundos en una máquina de escritorio corriente, lo bastante rápido como para que el codificador sea efectivamente gratis en cualquier aplicación que vayas a escribir. El consejo de rendimiento que realmente cambia código tiene que ver con la forma. La salida es una cadena de C#, y una cadena de C# almacena dos bytes por carácter, así que el coste en memoria de un resultado codificado es aproximadamente 2,7 bytes por byte de entrada (4 caracteres por 3 bytes de entrada, a 2 bytes por carácter), que es un número que conviene conocer cuando el payload va en megabytes. Si codificas miles de payloads pequeños en un bucle, prefiere las APIs span y de buffer de char, que escriben en buffers que reutilizas, sobre las APIs de cadena, que asignan una cadena gestionada nueva por llamada. Si codificas un solo archivo grande, omite la cadena por completo y usa la transform en streaming, porque el coste de 2 bytes por carácter de mantener una cadena de 13 megabytes es un derroche puro cuando un CopyTo habría mantenido el conjunto de trabajo en buffers de stream. Y si produces salida envuelta en MIME, recuerda que el paso de envolver es un segundo recorrido sobre los datos, así que envuelve solo cuando el transporte lo necesite, no por defecto.

La conversación de seguridad

El lado del codificador de Base64 tiene una lección de seguridad, y es la inversa de la del decodificador: tú eres quien hace la elección de exponer datos legibles, y el formato no te va a detener. Base64 es codificación, no cifrado. No tiene clave, no tiene algoritmo y no tiene secreto de ningún tipo, y la salida de tu llamada a ToBase64String está a un comando de la entrada, en cualquier máquina, en cualquier lenguaje, por cualquiera. Así que la primera regla es sobre lo que eliges codificar: nunca metas una contraseña, un token o un secreto en un archivo de configuración "protegido" por Base64, porque la protección queda a una sola llamada de decodificación, y la persona que lee la configuración tiene el comando. Si el valor debe ser secreto, necesita protección real, y el Base64 es solo la forma que viste mientras está en el campo de texto.

La segunda lección es sobre el canal, y es específica de las cosas que este artículo construye. Un encabezado de autenticación Basic lleva la contraseña en una forma que cualquier proxy, cualquier log y cualquier middlebox puede leer, que es por lo que el esquema solo es aceptable sobre TLS y está en gran parte obsoleto fuera de integraciones legacy. Una data URI en HTML lleva la imagen, y si la imagen es un SVG aportado por el usuario, lleva lo que el SVG lleve, que es por lo que el caso de SVG-en-data-URI necesita el mismo cuidado que cualquier contenido de usuario. Y un valor Base64 en una URL está, literalmente, en la URL, lo que significa que está en el historial del navegador, en el log de acceso del servidor, en el encabezado referrer y en la caché del proxy, así que los tokens que deben mantenerse privados no pertenecen en cadenas de consulta, con padding o sin él. El codificador hace su trabajo honesto en los tres casos, convirtiendo bytes en una cadena segura de llevar. La seguridad está en lo que llevas, y a dónde, y el formato es un mensajero mejor que la mayoría, pero es un mensajero, no una bóveda.

Trampas en las que caen los codificadores de C#

Estas son las trampas que siguen apareciendo en el lado de codificación del código C#, y cada una tiene una causa concreta en cómo funciona el framework:

  • El charset que no elegiste. Codificar una cadena con Encoding.Default produce Base64 distinto en .NET Framework (la página de código ANSI de Windows) y en .NET (UTF-8). Ambas salidas son válidas, ambas decodificarán "correctamente" en su plataforma de origen, y no son los mismos bytes. Fija la codificación explícitamente.
  • Doble codificación. La entrada ya era Base64 (una configuración que codificó un valor ya codificado, una API que recodifica su entrada), y el codificador, haciendo exactamente lo que le dijeron, produjo Base64-de-Base64. El resultado parece plausible, y decodifica una capa a la vez, que es como se descubre en producción un bug que necesita dos decodificaciones para arreglarse.
  • Saltos de línea en el lugar equivocado. La forma envuelta en MIME, con sus pares CRLF, cae en una cadena JSON, un segmento de JWT o un parámetro de URL, donde el consumidor estricto se atraganta con los espacios en blanco que nunca le dijeron que esperara. Envuelve para el correo, déjalo solo en todas partes, y si quitas el envoltorio de otro, quita el \r además del \n.
  • Alfabeto estándar en una URL. Un + en una cadena de consulta se decodifica como espacio por las reglas de análisis de formularios, así que un valor Base64 estándar metido en una URL vuelve con letras donde estaban los signos de más. Usa el alfabeto URL-safe, o codifica por porcentaje el valor entero, y nunca ambos.
  • El desajuste de padding. Tu salida tiene padding, el consumidor quiere compacto, o al revés, y ninguna de las dos partes tiene la culpa - solo no están de acuerdo. La corrección es la aritmética de la sección de padding, aplicada en el lado que conoce la expectativa del consumidor, que suele ser el lado que escribe el token.
  • Memoria que no se presupuestó. La cadena codificada es dos bytes por carácter en memoria, así que un archivo de 10 MB se vuelve una cadena de 13 millones de caracteres que pesa unos 27 MB en memoria gestionada, y un bucle que construye cadenas así una a una aparecerá en el profiler como agitación de asignaciones sin causa visible. Dimensiona los buffers con los helpers de longitud, haz streaming de los grandes, reutiliza buffers en los bucles calientes.
  • La transform que no envuelve. ToBase64Transform emite una sola línea larga. Un código que hace streaming de un adjunto "listo para MIME" a través de ella y luego lo envía por correo produce una línea de 120.000 caracteres que algún transporte reenvolverá en medio de un grupo, que es exactamente la corrupción que la regla de 76 caracteres estaba diseñada para prevenir.
  • Codificar la codificación. Pasar una cadena Base64 al codificador porque "los datos ya son texto" produce una segunda capa. El codificador no sabe, y no le importa, que su entrada se parece a Base64; codifica los caracteres que la cadena tenga, y el decodificador del otro lado recibe una cadena Base64 donde esperaba tus datos.

Cómo creció el codificador: un recorrido por versiones

El lado de codificación de la API tiene su propia línea de tiempo, y va de la segunda versión de .NET a la que ahora mismo está en preview:

  • .NET Framework 1.1, abril de 2003. Llegan Convert.ToBase64String y ToBase64CharArray, la familia clásica completa en una sola versión, con las sobrecargas de porción ya incluidas, que es un pequeño milagro de previsión para una API de 2003.
  • .NET 2.0, 2005. Base64FormattingOptions y el valor InsertLineBreaks se unen a la familia, trayendo el envoltorio de líneas MIME al framework y poniendo fin a una era de bucles de Substring hechos a mano en el código de email.
  • .NET Core 2.1, 2018. La era de los span. Convert gana la codificación basada en span y TryToBase64Chars, y llega la nueva clase System.Buffers.Text.Base64 con su contrato de OperationStatus y la inflación in-place, construida para el mundo de asignación cero.
  • .NET 5, 2020. Salen los hermanos hex (Convert.ToHexString y compañía), el mismo patrón de diseño aplicado a un alfabeto de 16 símbolos, y el patrón de clase de conversión se convierte en un estilo de la casa.
  • .NET 7, 2022. X509Certificate2.ExportCertificatePem hace que el framework produzca PEM por ti, marcadores de armadura, envoltorio de 64 caracteres y cuerpo Base64 incluidos, lo que jubila en silencio una clase de código manual de formateo de certificados.
  • .NET 9, noviembre de 2024. System.Buffers.Text.Base64Url por fin llega a la caja después de años de peticiones de la comunidad, con el paquete Microsoft.Bcl.Memory retroportándolo a .NET Framework 4.6.2 y superior, y el comportamiento sin padding que el código de JWT llevaba haciendo a mano todo el tiempo.
  • .NET 11, en preview en el momento de escribir esto. La siguiente versión, prevista para finales de 2026, añade más APIs y sobrecargas de conveniencia de Base64 a los tipos existentes, continuando la marcha hacia una superficie más ergonómica.

El formato en sí tiene una biografía más antigua, y es la razón por la que la API de C# se parece a lo que es. El primer uso estandarizado de lo que ahora llamamos MIME Base64 fue el protocolo Privacy-Enhanced Mail en 1987 (RFC 989), la especificación MIME fijó la forma envuelta a 76 caracteres en 1993, y el RFC 4648 en 2006 dio al formato su especificación moderna y consciente del alfabeto, incluida la variante URL-safe para la que C# no tuvo un codificador de primera clase hasta 2024. Tres décadas de convenciones de email y web son por lo que los saltos de línea, el padding y los dos alfabetos existen, y el codificador de C# es el lugar donde los tres se encuentran.

Pequeñas maravillas

  • El mínimo de cuatro caracteres. La salida Base64 no vacía más pequeña posible tiene cuatro caracteres, porque el formato piensa en grupos de cuatro aunque le des un solo byte. Un byte de cualquier cosa codifica a dos letras y dos signos =, y esa forma, dos caracteres de datos con sombrero de padding, es una huella que empezarás a reconocer en configuraciones y tokens.
  • Los nulos son bienvenidos. El codificador no tiene opinión sobre lo que significan los bytes, así que un buffer lleno de ceros codifica con gusto a un muro de caracteres A, y un archivo binario con sus bytes NUL intactos hace el viaje de ida y vuelta sin perder uno solo. La ansiedad de "las cadenas no pueden contener binario" pertenece al lado de las cadenas del sistema de tipos, no al codificador, que nunca ve una cadena.
  • El determinismo como función. Los mismos bytes, las mismas opciones, siempre la misma cadena. No hay marca de tiempo, no hay sal aleatorio, no hay variación, que es por lo que una cadena Base64 hace de huella rápida y sucia, y bastante usable, de los contenidos de un archivo: dos archivos con el mismo Base64 son el mismo archivo, y la comprobación es una comparación de cadenas.
  • Dos bytes por carácter, sin coste. Una cadena de C# es UTF-16, así que cada carácter de tu salida Base64 ocupa dos bytes en memoria gestionada. El codificador no lo anuncia, la propiedad de longitud no lo informa, y una cadena de 13 millones de caracteres simplemente pesa 26 MB, que es el número que debes tener en la cabeza cuando el payload es grande.
  • CRLF por herencia. El envoltorio MIME inserta pares de retorno de carro y salto de línea incluso cuando tu código corre en Linux, porque la regla viene de la especificación de email, no de la plataforma. El codificador es un historiador tanto como un conversor, y preserva los finales de línea de 1993 en una máquina de 2026.
  • Una sobrecarga de porción desde el día uno. ToBase64String(byte[], int, int) codifica una ventana a un array más grande desde 2003, quince años antes de que los span hicieran la idea de moda. Los diseñadores de API de la era de la 1.1 miraron buffers reales y añadieron la forma de desplazamiento-y-longitud, y sigue siendo la decisión correcta cuando los datos son una sección de una lectura más grande.
  • La línea de certificado de 64 caracteres. PEM envuelve a 64 caracteres, no a 76, y ExportCertificatePem lo sabe y envuelve en consecuencia, que es uno de los detalles silenciosos que hace de "deja que el framework lo haga" el consejo correcto para el trabajo con certificados. Dos anchos de envoltorio, una familia de formato, y el framework los mantiene en orden.
  • Dos alfabetos, dos nombres. Los 64 valores se llaman "estándar" en una parte de la API y "URL-safe" en otra, y difieren en exactamente dos caracteres: los huecos 62 y 63. + y / de un lado, - y _ del otro, y cada bug de interoperabilidad de este artículo vive en el momento en que alguien dio por hecho que los dos lados eran lo mismo.

Dando la vuelta completa

Esa es la parte del codificador, y es donde tomas las decisiones: el alfabeto, el padding, los saltos de línea, el charset, el buffer. La otra dirección, recibir Base64 de otras personas, con sus decisiones de padding, sus saltos de línea, sus alfabetos y sus tokens, es donde vive la mayor parte del dolor, porque no puedes negociar con un payload. La decodificación Base64 en C#, desde el clásico de una línea hasta las familias span y URL-safe, se cubre a fondo en el artículo complementario enlazado abajo, y entre los dos el tema entero cabe en tu memoria de trabajo, que es el punto de un formato tan antiguo y tan pequeño.

Última actualización: 2026-09-08

Artículo relacionado: Decodificación Base64 en C# (CSharp): una guía completa