¿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.

Decodificación Base64 en C++ (Cpp): una guía completa

Ya tienes la cadena. Una larga cinta de letras y dígitos, el ocasional + o /, quizá un = o dos al final, y en algún rincón de tu ticket, contrato o columna de base de datos, la promesa de que es Base64. Ahora necesitas los bytes originales de vuelta, en C++, y necesitas que salgan bien. La página de inicio de este sitio repasa el formato en profundidad, así que aquí va solo la versión corta: cuatro caracteres del alfabeto cargan tres bytes, un final de uno o dos = marca dónde se acabaron los datos reales, y la forma codificada es aproximadamente un 33 por ciento más grande que el original. Decodificar es la dirección que encoge, así que un decodificador nunca puede necesitar más memoria que el payload que ya tiene en la mano. Esa es una propiedad genuinamente agradable, y una de las alegrías silenciosas de trabajar en esta dirección.

El titular grande es que C++ en sí no decodificará ni un solo carácter por ti. La biblioteca estándar ha tenido treinta años para echar a crecer una función base64 y los ha gastado todos en otras cosas, así que cada programa en C++ trae su propio decodificador de entre un banquillo de tres personalidades muy distintas, más la opción de escribir unas cuarenta líneas propias. Uno es un caballo de batalla que ha llevado a internet desde los 90, otro es un tipo callado que se para a mitad de frase y no dice una palabra al respecto, y otro es un purista que lanza una excepción ante un solo espacio suelto. Cuando sepas lo que cada uno perdona, lo que se niega a aceptar y lo que hace en silencio a tus espaldas, decodificar deja de ser una fuente de bugs misteriosos. Abramos algunos paquetes.

La caja de herramientas: cuatro decodificadores, cuatro temperamentos

Aquí tienes el panorama de un vistazo. Los cuatro manejan el alfabeto estándar; las diferencias están en los bordes, y en los bordes es donde viven los bugs.

Decodificador De dónde viene Modelo de errores Particularidad a recordar
OpenSSL EVP <openssl/evp.h>, enlaza -lcrypto Devuelve -1 con entrada rota La versión one-shot rellena el final con ceros
Boost.Beast <boost/beast/core/detail/base64.hpp>, header-only Ninguno: simplemente se detiene Ningún canal de error de ninguna clase
Iteradores de Boost.Serialization <boost/archive/iterators/binary_from_base64.hpp>, header-only Lanza dataflow_exception Trata = como un cero de verdad
Tus propias cuarenta líneas En ninguna parte: son tuyas A tu elección, hasta la posición del byte Tú eres el dueño de cada caso límite para siempre

La instalación es un nombre de paquete por distro. Para OpenSSL: libssl-dev en Debian y Ubuntu, openssl-devel en Fedora y RHEL, openssl en Arch, y brew install openssl en macOS. Para Boost, cuya versión actual es la 1.92.0 de agosto de 2026 en un proyecto que lleva construyendo bibliotecas desde 1998: libboost-dev o boost-devel. Ambos decodificadores de Boost de más abajo son header-only, así que no hay nada que enlazar. Si tu proyecto se basa en CMake, toda la configuración son tres líneas:

find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)

Una nota de versión antes del código, porque cambia lo que devuelve tu decodificador. OpenSSL 3.5, publicada en abril de 2025 como línea de soporte a largo plazo, arregló un bug real en el decodificador por streaming (más en un momento), y la versión 4.0 de nuevas características, más reciente, de abril de 2026 heredó el arreglo. Si tu build fija una 3.0 o 3.3 antigua, lee el párrafo de versión de la sección de OpenSSL de más abajo antes de confiar en una longitud de final.

OpenSSL: el decodificador que probablemente ya enlazas

Si tu programa en C++ roza TLS, hashing o certificados, OpenSSL ya está dentro del binario, y sus rutinas base64 de EVP son los decodificadores más probados en batalla del oficio. La función one-shot es una sola llamada:

int EVP_DecodeBlock(unsigned char *t, const unsigned char *f, int n);

Le pasas un buffer de caracteres base64 y su longitud, y escribe los bytes decodificados en t. Recorta el espacio en blanco inicial, recorta el espacio en blanco final, saltos de línea y retornos de carro, y luego aplica reglas sin concesiones: nada de espacio en blanco interno, y la longitud recortada debe ser múltiplo de 4. Cada cuatro caracteres de entrada producen exactamente tres bytes de salida, y aquí viene la parte que sorprende: los caracteres de padding se decodifican como seis bits a cero, y la man page anota con calma que es responsabilidad de quien llama la función tener en cuenta el padding final. En otras palabras, la función hace la aritmética por ti y luego añade en silencio hasta dos bytes bonus de ceros al final. El wrapper idiomático de C++ esconde la matemática de buffers detrás de un std::string:

#include <cstddef>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>

std::string openssl_decode(const std::string &b64) {
  std::vector<unsigned char> out(b64.size() * 3 / 4 + 4);
  int n = EVP_DecodeBlock(out.data(),
                          reinterpret_cast<const unsigned char *>(b64.data()),
                          static_cast<int>(b64.size()));
  if (n < 0) return {};
  size_t pads = 0;
  for (size_t i = b64.size(); i > 0 && b64[i - 1] == '='; --i) ++pads;
  return std::string(reinterpret_cast<const char *>(out.data()),
                     static_cast<size_t>(n) - pads);
}

int main() {
  std::string one = openssl_decode("TQ==");
  std::printf("%zu bytes: %02x\n", one.size(),
              static_cast<unsigned char>(one[0]));
  std::string word = openssl_decode("TWFuZQ==");
  std::printf("%zu bytes: %s\n", word.size(), word.c_str());
}

Ejecútalo y el relleno de ceros aparece exactamente donde la documentación prometía: decodificar la cadena de cuatro caracteres TQ== da tres bytes, la letra M más dos ceros, antes de que el wrapper los recorte. Aliméntalo con TWFuZQ== y obtienes los limpios cuatro bytes de "Mane". Aliméntalo con un carácter fuera del alfabeto y te devuelve una cadena vacía, porque la función respondió con -1. Fíjate en lo que std::string hace en silencio dentro de este wrapper: lleva la cuenta de su propia longitud y contiene bytes a cero sin problema, así que un JPEG decodificado puede vivir dentro de tu tipo de texto y ser comparado, hasheado y pasado de mano en mano. En C estarías contándole tus bendiciones a una variable de longitud; aquí la cadena simplemente funciona.

Para datos que llegan en trozos, OpenSSL te entrega un contexto al que le alimentas fragmentos y del que extraes resultados, y las tres funciones tienen un vocabulario compacto de valores de retorno:

Llamada Devuelve Qué significa
EVP_DecodeUpdate -1 Carácter no válido, o un carácter de padding en medio de los datos
EVP_DecodeUpdate 1 Se espera más entrada
EVP_DecodeUpdate 0 Fin de datos: el último grupo traía padding, o apareció el marcador suave de fin de entrada
EVP_DecodeFinal 1 / -1 El flujo terminó limpio / los caracteres residuales no eran múltiplo de 4

Dos comportamientos hacen del decodificador por streaming el más perdonavidas de la caja de herramientas. Salta espacios, tabulaciones, retornos de carro y saltos de línea en cualquier parte del flujo, así que un bloque de email MIME con líneas CRLF de 76 caracteres pasa a través exactamente como una cadena de una línea, e informa del recuento real de bytes: el mismo TQ== que engañó a la función one-shot te da exactamente un byte aquí, sin aritmética necesaria. Mastica la entrada en fragmentos de hasta 64 caracteres base64, trabajando desde un buffer interno de 80 bytes, y guarda en buffer lo que no entra en un grupo de cuatro, por lo que puedes alimentarlo en tamaños de trozo arbitrarios. El wrapper:

#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>

std::string openssl_decode_stream(const std::string &b64) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  EVP_DecodeInit(ctx);
  std::string out;
  std::vector<unsigned char> chunk(1024);
  int outl = 0;
  for (size_t pos = 0; pos < b64.size(); pos += chunk.size()) {
    size_t take = std::min(chunk.size(), b64.size() - pos);
    int ret = EVP_DecodeUpdate(ctx, chunk.data(), &outl,
                               reinterpret_cast<const unsigned char *>(b64.data()) + pos,
                               static_cast<int>(take));
    if (ret < 0) {
      EVP_ENCODE_CTX_free(ctx);
      return {};
    }
    out.append(reinterpret_cast<const char *>(chunk.data()), outl);
    if (ret == 0) break;
  }
  unsigned char tail[3];
  int tail_l = 0;
  int fin = EVP_DecodeFinal(ctx, tail, &tail_l);
  EVP_ENCODE_CTX_free(ctx);
  if (fin != 1) return {};
  out.append(reinterpret_cast<const char *>(tail), tail_l);
  return out;
}

Y ahora la nota al pie de versión de la tabla de la caja de herramientas, porque es exactamente el tipo de cosa que convierte un ticket "resuelto" en abierto de nuevo: en todas las versiones de OpenSSL anteriores a 3.5, la ruta por streaming tenía el mismo hábito de rellenar con ceros que el decodificador por bloques. Se reportó en febrero de 2025 como issue 26677; el commit del arreglo aterrizó en master el 27 de febrero de 2025, y el pull request asociado se cerró el mismo día. La man page oficial lo registra ahora en su sección de historia: a partir de OpenSSL 3.5, EVP_DecodeUpdate produce el número de bytes que la documentación siempre reclamó y ya no decodifica el padding como bits a cero. Si tu base de código fija una OpenSSL antigua y tus longitudes de final parecen desviarse en uno o dos bytes, esto es lo primero que debes revisar. Y hay una excentricidad heredada de la época de PEM: el guion - no es un carácter del alfabeto en absoluto - es un marcador suave de fin de entrada. Si tu flujo contiene uno después de un múltiplo de 4 caracteres válidos, el decodificador devuelve 0 y te pide que pares, y por eso una cadena base64url que necesita de verdad sus caracteres - no se va a decodificar simplemente - transcódificalo primero, en la sección de más abajo, y el viajero del tiempo desaparece.

Boost.Beast: el decodificador rápido que nunca se queja

Si Boost ya está en el proyecto, su biblioteca HTTP trae un codec base64 en la improbable dirección boost/beast/core/detail/base64.hpp. El espacio de nombres detail:: es la forma de Boost de decir "esto es nuestro asunto interno", y los mantenedores se han negado a promoverlo a API pública. Todo el mundo lo usa de todas formas, porque es pequeño, rápido y header-only: define BOOST_BEAST_HEADER_ONLY antes del include y no hay nada que enlazar. Es también, por cierto, el codec que el propio handshake de WebSocket de Boost.Beast usa para el cómputo de Sec-WebSocket-Accept, así que lleva años masticando tráfico real.

La personalidad de la función de decodificación es la sorpresa. Toma tu buffer de salida, la entrada y su longitud, y devuelve un par: el número de octetos escritos y el número de caracteres leídos. Se detiene en el primer =, y en el primer carácter no válido - y en ambos casos lo hace sin avisarte. No hay código de error, no hay excepción, no hay bandera de estado. Un payload corrompido, un payload con saltos de línea y un final truncado producen todos un resultado parcial exitoso:

Entrada Bytes decodificados Caracteres leídos Por qué se detuvo
"TWFuZQ==" 4 bytes "Mane" 6 Se detuvo en el primer =
"TWF!ZQ==" 2 bytes "Ma" 3 Se detuvo en !
"TWFu\nZQ==" 3 bytes "Man" 4 Se detuvo en \n
"TWFuZQ" 4 bytes "Mane" 6 El final estaba truncado
"TQ==" 1 byte "M" 2 El padding se maneja sin problema

Lee esa tabla una segunda vez, porque es todo el modelo de amenazas de un decodificador que nunca se queja: hizo lo mejor que pudo, se detuvo donde se detuvo, y tocarte a ti notarlo. La comprobación tiene un detalle: para entrada con padding, el recuento de "caracteres leídos" se detiene en el primer =, así que sumas los pads de nuevo antes de comparar, y también exiges que el total sea múltiplo de cuatro, porque esa es la única forma que tiene un payload real:

#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <string>

namespace b64 = boost::beast::detail::base64;

std::string beast_decode(const std::string &in) {
  std::string out;
  out.resize(in.size() / 4 * 3 + 3); /* margen para longitudes raras */
  auto result = b64::decode(out.data(), in.data(), in.size());
  out.resize(result.first);
  size_t pads = 0;
  for (size_t i = in.size(); i > 0 && in[i - 1] == '='; --i) ++pads;
  if (result.second + pads != in.size() || in.size() % 4 != 0)
    return {}; /* se detuvo antes de tiempo, o el final era imposible */
  return out;
}

Dos detalles para archivar. Primero, el helper decoded_size(n) al que la cabecera te apunta solo es una cota superior válida cuando n es múltiplo de 4 - el propio comentario de la función lo dice - y por eso el wrapper de más arriba añade un par de bytes de margen en vez de fiarse de él para longitudes arbitrarias. Segundo, la procedencia: los archivos de origen llevan un copyright 2016-2019 de Vinnie Falco, con un pie de página que atribuye partes a un fragmento de 2004-2008 de Rene Nyffenegger. Ese fragmento es el par base64 que se ha copiado y pegado por internet anglosajón durante dos décadas, y ahora se distribuye dentro de Boost, en tu binario, haciendo auth Basic de HTTP para toda la web.

Boost.Serialization: el decodificador que lanza una excepción ante un espacio suelto

La biblioteca de serialización de Boost lleva el base64 más antiguo del ecosistema de C++: un conjunto de adaptadores de iteradores componibles de 2002, autoría de Robert Ramey, que tratan el "sé liberal en lo que aceptes" como una ofensa personal. La dirección de decodificación vive en binary_from_base64.hpp (sí, el nombre es desde el punto de vista de la salida) y se empareja con un transformador de anchura que repaqueta valores de seis bits en bytes de ocho:

#include <boost/archive/iterators/binary_from_base64.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <string>

namespace it = boost::archive::iterators;

std::string boost_decode(const std::string &in) {
  using dec =
      it::transform_width<it::binary_from_base64<const char *>, 8, 6>;
  std::string out(dec(in.data()), dec(in.data() + in.size()));
  size_t pads = 0;
  for (size_t i = in.size(); i > 0 && in[i - 1] == '='; --i) ++pads;
  out.resize(out.size() - pads);
  return out;
}

El iterador interno convierte cada carácter base64 en su valor de seis bits, y el externo reagrupa esos valores en bytes. Su estrictez es total: cualquier carácter fuera del alfabeto - incluyendo un solo espacio - hace que el iterador lance boost::archive::iterators::dataflow_exception con el mensaje "attempt to decode a value not in base64 char set". Ese es el comportamiento de "rechazar a menos que se diga lo contrario" que los RFC hicieron explícito más tarde - implementado en 2002, un año entero antes de que el RFC 3548 codificara la misma regla. La consecuencia práctica es que la entrada envuelta en MIME debe liberarse de sus saltos de línea antes de tocar este iterador. La segunda rareza es más sutil: en la tabla de búsqueda, el carácter de padding = no se salta - se convierte en el valor cero. Decodificar TWFuZQ== produce por tanto seis bytes - 4d 61 6e 65 00 00 - porque ambos caracteres de padding aportaron datos reales (a cero), y la línea resize(size - pads) del fragmento es estructural, no decorativa. Decodifica TQ== y obtienes tres bytes que se recortan hasta la sola letra M, exactamente donde quieres aterrizar.

Cuarenta líneas que son tuyas

Base64 es lo bastante pequeño para que un decodificador correcto sea una cosa respetable de poseer, y en C++ la recompensa es mejor que en cualquier otro lenguaje: std::string hace que la gestión de buffers sea agradable, y un decodificador hecho a mano puede hacer algo que ninguna de las versiones de biblioteca de arriba hace, que es señalar el byte exacto que dolió. Esta versión sigue la lectura estricta del RFC 4648 - recortar los extremos, rechazar el espacio en blanco interno, rechazar el padding en medio, aplicar las reglas de longitud, e incluso comprobar los bits de padding que el RFC dice que un codificador conforme debe haber puesto a cero:

#include <cstddef>
#include <cstring>
#include <string>

std::string strict_decode(const std::string &in, size_t *error_pos = nullptr) {
  static const char *table =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  auto fail = [&](size_t pos) {
    if (error_pos) *error_pos = pos;
    return std::string();
  };
  size_t start = 0;
  size_t end = in.size();
  while (start < end && (in[start] == ' ' || in[start] == '\t' ||
                         in[start] == '\r' || in[start] == '\n'))
    ++start;
  while (end > start && (in[end - 1] == ' ' || in[end - 1] == '\t' ||
                         in[end - 1] == '\r' || in[end - 1] == '\n'))
    --end;
  size_t pads = 0;
  while (end > start && in[end - 1] == '=') {
    --end;
    ++pads;
  }
  size_t body = end - start;
  if (pads > 2 || (pads == 1 && body % 4 != 3) ||
      (pads == 2 && body % 4 != 2) || (pads == 0 && body % 4 == 1))
    return fail(in.size());
  if (body >= 2 && body % 4 != 0) {
    int leftover = static_cast<int>((body % 4) * 6 % 8);
    int last = static_cast<int>(std::strchr(table, in[end - 1]) - table);
    if ((last & ((1 << leftover) - 1)) != 0)
      return fail(end - 1); /* bits de padding no canónicos */
  }
  int value = 0;
  int bits = -8;
  std::string out;
  out.reserve(body / 4 * 3);
  for (size_t i = start; i < end; ++i) {
    const char *p = std::strchr(table, in[i]);
    if (!p)
      return fail(i);
    value = (value << 6) + static_cast<int>(p - table);
    bits += 6;
    if (bits >= 0) {
      out.push_back(static_cast<char>((value >> bits) & 0xFF));
      bits -= 8;
    }
  }
  return out;
}

Repasemos lo que aplica. El espacio en blanco inicial y final se recorta, porque un payload copiado de una cabecera de email es muy probable que llegue usando uno. El espacio en blanco interno se rechaza, porque el RFC 4648 dice que un decodificador DEBE rechazar los caracteres no alfabéticos a menos que la especificación que lo rodea diga lo contrario, y en un límite de seguridad quieres la lectura estricta. Un carácter de padding en medio de los datos se rechaza, al igual que cualquier longitud que no pueda corresponder a un payload real: un carácter menos que un grupo es imposible, y un solo pad solo es legal detrás de tres caracteres de cuerpo. La comprobación canónica del final es la que la mayoría de las implementaciones saltan: si el último grupo tenía uno o dos caracteres de padding, los bits bajos no usados del último carácter del alfabeto deben ser cero, o los mismos bytes podrían escribirse como dos cadenas visiblemente distintas. Esa maleabilidad es la razón de que la comprobación exista, y cuesta cuatro líneas. Por último, el decodificador acepta entrada sin padding, que es exactamente lo que son los segmentos de un JWT. Y al fallar te entrega la posición: aliméntalo con TWF!ZQ== y el error se sienta en el índice 3, sobre el signo de exclamación, que es la diferencia entre reportar un bug y arreglarlo.

Base64url: el alfabeto que hablan los tokens

El alfabeto estándar tiene dos caracteres que no sobreviven a una URL: + significa espacio en una query string, y / significa directorio en una ruta. La sección 5 del RFC 4648 arregla esto con dos intercambios de caracteres - + se convierte en - y / se convierte en _ - y es tajante con el resultado: esta codificación "no debe considerarse igual a la codificación base64". Es el alfabeto de los JWTs, de los code challenges de OAuth PKCE, de los identificadores de vídeo de YouTube y de la mayoría de tokens de API, y con frecuencia suelta también el padding de =, porque en un token la longitud se conoce implícitamente y el padding sería solo un percent-escape a la espera de pasar. Ninguno de los decodificadores de C++ de arriba lo habla de forma nativa - OpenSSL incluso trata una - como ese marcador suave de fin de entrada - así que la solución es un pequeño transcode antes de decodificar. Es tan corto que se lleva fácil en la cabeza:

#include <string>

std::string url_to_standard(std::string in) {
  for (char &c : in) {
    if (c == '-') c = '+';
    else if (c == '_') c = '/';
  }
  switch (in.size() % 4) {
    case 2: in += "=="; break; /* restaurar el padding desechado */
    case 3: in += '=';  break;
    default: break;
  }
  return in;
}

Aplicado a un JWT real, los dos primeros segmentos son JSON plano a la espera de pasar. Un token de ejemplo clásico decodifica a una cabecera de {"alg":"HS256","typ":"JWT"} y un conjunto de claims que contiene un subject, un nombre y un timestamp de emisión. El tercer segmento decodifica igual y te da los bytes crudos de la firma - no es texto, y no es prueba de nada. Decodificar un token te dice lo que afirma; verificar la firma te dice si hay que creerle, y eso es un trabajo de criptografía que ninguna biblioteca base64 hará por ti. En Windows la situación es unilateral, en la misma dirección: CryptBinaryToStringA tiene una bandera CRYPT_STRING_BASE64URI para el lado de la codificación, pero la dirección de decodificación no tiene ninguna bandera URL-safe, así que el transcode de arriba gana su sitio en tu memoria muscular en todas las plataformas.

De bytes de vuelta a texto

Pregúntale a un decodificador de C++ "¿qué charset he decodificado ahora mismo?" y obtienes la respuesta más honesta que tiene el lenguaje: ninguna. Los decodificadores son orientados a bytes de principio a fin. No ven texto; ven bytes, y te devuelven exactamente los bytes que estaban empaquetados. Si el original era UTF-8, ahora tienes UTF-8, y no se necesita nada más. El giro bonito de C++ es que el propio std::string es un contenedor de bytes con un miembro de longitud, así que el modo de fallo clásico de C - una función de cadena que se detiene en el primer NUL - se evapora casi por completo. Un archivo decodificado, un certificado decodificado, una imagen decodificada: todos ellos pueden vivir en un string, ser comparados con ==, hasheados y pasados por valor, y los bytes a cero de dentro son simplemente bytes. Solo no conviertas a cadena de C y luego la midas con strlen; usa size().

Para las codificaciones heredadas que todavía acechan en bases de datos viejas, exportaciones y herramientas escritas a mano - ISO-8859-1, Windows-1252 y sus parientes - la herramienta estándar es el iconv de POSIX, que viene con glibc. Primero decodifica a bytes, y luego convierte esos bytes a UTF-8 con el codec que corresponda al origen:

#include <iconv.h>
#include <cstddef>
#include <string>
#include <vector>

std::string to_utf8(const std::vector<unsigned char> &raw,
                    const char *source_charset) {
  iconv_t cd = iconv_open("UTF-8", source_charset);
  if (cd == (iconv_t)-1) return {};
  char *inptr = reinterpret_cast<char *>(const_cast<unsigned char *>(raw.data()));
  size_t inleft = raw.size();
  std::vector<char> utf8buf(raw.size() * 4 + 8);
  char *outptr = utf8buf.data();
  size_t outleft = utf8buf.size();
  if (iconv(cd, &inptr, &inleft, &outptr, &outleft) == (size_t)-1) {
    iconv_close(cd);
    return {};
  }
  iconv_close(cd);
  return std::string(utf8buf.data(),
                     static_cast<size_t>(outptr - utf8buf.data()));
}

El viaje de ida y vuelta es sin pérdida en ambas direcciones: empaqueta una cadena como ISO-8859-1, hazla base64, envíala, decodifícala, conviértela, y obtienes exactamente con lo que empezaste, con los caracteres acentuados intactos. Y los datos binarios no tienen charset en absoluto - un PNG es un PNG te guste o no, que es la respuesta más liberadora de todo el artículo.

Decodificar archivos

Los archivos pequeños son una coreografía de cuatro pasos: abrir en binario, leer en un vector, decodificar, escribir el resultado de nuevo en binario. Modo binario, siempre, en todas las plataformas - en Windows una lectura en modo texto traduciría las parejas CRLF a saltos de línea simples y cambiaría tus datos en silencio antes de que el decodificador ni siquiera los viera:

#include <fstream>
#include <iterator>
#include <string>
#include <vector>

std::vector<unsigned char> read_file(const std::string &path) {
  std::ifstream in(path, std::ios::binary);
  return {std::istreambuf_iterator<char>(in),
          std::istreambuf_iterator<char>()};
}

Fíjate en las llaves del código de arriba. Con paréntesis simples, una línea de la forma std::vector<unsigned char> bytes(istreambuf_iterator<char>(file), istreambuf_iterator<char>()) es la célebre "most vexing parse": el compilador la lee como la declaración de una función que devuelve un vector, y tiene todo el derecho a hacerlo. La forma con inicializador entre llaves de arriba esquiva la gramática por completo. Una vez que tienes los bytes en la mano, pásalos por cualquier decodificador de este artículo y escribe el resultado con std::ofstream en std::ios::binary, usando write(data.data(), data.size()) en lugar del operador de stream, para que cualquier byte a cero embebido sobreviva el viaje al disco. Un archivo .b64 y su gemelo decodificado difieren entonces exactamente por el impuesto del 33 por ciento que pagaste en la entrada, lo que da para un momento de checksum muy satisfactorio.

Archivos grandes: streaming en ambas direcciones

Para archivos demasiado grandes para caber en memoria, el decodificador por streaming de la sección de OpenSSL hace todo el trabajo: leer un trozo, pasarlo por el contexto, escribir lo que salió, y repetir. En cualquier momento solo un pequeño buffer vive en RAM, así que un archivo base64 de 10 GB se decodifica con el mismo código que uno de 10 KB, y el salto de líneas al estilo MIME no necesita preprocesamiento en el camino, porque el decodificador por streaming encoge los hombros ante los saltos de línea:

#include <fstream>
#include <string>
#include <vector>
#include <openssl/evp.h>

bool decode_stream_to_file(const std::string &in_path,
                           const std::string &out_path) {
  std::ifstream in(in_path, std::ios::binary);
  std::ofstream out(out_path, std::ios::binary);
  if (!in || !out) return false;
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  EVP_DecodeInit(ctx);
  std::string chunk(65536, '\0');
  std::vector<unsigned char> decoded(49152);
  bool ok = true;
  for (;;) {
    std::streamsize got = in.read(chunk.data(), chunk.size()).gcount();
    if (got < 0) { ok = false; break; }
    if (got == 0) break;
    int outl = 0;
    int ret = EVP_DecodeUpdate(ctx, decoded.data(), &outl,
                               reinterpret_cast<const unsigned char *>(chunk.data()),
                               static_cast<int>(got));
    if (ret < 0) { ok = false; break; }
    out.write(reinterpret_cast<const char *>(decoded.data()), outl);
    if (ret == 0) break;
  }
  unsigned char tail[3];
  int tail_l = 0;
  if (ok && EVP_DecodeFinal(ctx, tail, &tail_l) != 1)
    ok = false;
  if (ok)
    out.write(reinterpret_cast<const char *>(tail), tail_l);
  EVP_ENCODE_CTX_free(ctx);
  return ok;
}

Los tamaños de buffer no son arbitrarios: los parámetros de longitud de las funciones EVP son int, así que una sola llamada es segura hasta 2 GB, y los números de arriba mantienen cada trozo a 64 KB de entrada con un buffer de salida de 48 KB, que es exactamente 3 de 4. Ese techo de int es toda la razón por la que existe la ruta por streaming, y vale la pena conocerlo como un hecho duro en lugar de descubrirlo como un bug de plataforma. Si la entrada resulta estar corrupta, la función devuelve false en el primer trozo que no puede decodificarse, y el archivo de salida retiene lo que era válido antes de eso - que, según tu pipeline, podría ser exactamente el resultado parcial que querías.

HTTP, APIs y los campos JSON que esconden bytes

Base64 aparece en HTTP en dos formas. La primera son datos: una respuesta JSON con un campo "certificate" o "avatar" lleno de base64, un endpoint de subida que acepta bytes en una columna segura para texto, un endpoint de descarga que te entrega un archivo .b64. El patrón siempre es el mismo - parsear el JSON, sacar la cadena, decodificarla, tratar el resultado como bytes - y el lado de decodificación de este artículo es toda la implementación. La segunda forma son credenciales: la cabecera Authorization: Basic es el base64 de user:password, y ha sido el único caso de uso de base64 del estándar durante treinta años. Analizarla lleva dos pasos, y el primero es donde la gente acude a una cadena de C terminada en NUL en mitad de datos cercanos al binario y se pregunta por qué:

#include <optional>
#include <string>

/* strict_decode de la sección "Cuarenta líneas que son tuyas" */

std::optional<std::pair<std::string, std::string>> parse_basic_auth(
    const std::string &b64) {
  std::string raw = strict_decode(b64);
  size_t colon = raw.find(':');
  if (colon == std::string::npos)
    return std::nullopt;
  return std::make_pair(raw.substr(0, colon), raw.substr(colon + 1));
}

Pásale el valor de la cabecera después del prefijo Basic , y te devuelve el usuario y la contraseña como cadenas de verdad con longitud registrada, o nada en absoluto si el payload no es una pareja user:pass. La nota de seguridad va aquí aunque no sea un tema de C++: el Basic auth es ofuscación, no protección. La cabecera viaja en claro para cualquiera que pueda leer la red, así que solo es aceptable detrás de TLS, y aun así es la elección para llamadas máquina a máquina, no para personas.

JWTs: leer lo que un token afirma

Un JSON Web Token es tres segmentos base64url pegados con puntos: cabecera, claims, firma. Los dos primeros son objetos JSON; el tercero es una firma criptográfica sobre la cadena header.claims, computada con el algoritmo que nombra la cabecera. C++ no tiene un tipo JWT incorporado, pero leer un token no necesita nada más que el transcode de la sección de base64url y un decodificador, porque la parte interesante es la lectura:

#include <cstddef>
#include <cstdio>
#include <string>

/* url_to_standard y strict_decode de secciones anteriores */

int main() {
  const std::string token =
      "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
      "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
      "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c";
  size_t dot1 = token.find('.');
  size_t dot2 = token.find('.', dot1 + 1);
  std::string header = strict_decode(
      url_to_standard(token.substr(0, dot1)));
  std::string claims = strict_decode(
      url_to_standard(token.substr(dot1 + 1, dot2 - dot1 - 1)));
  std::printf("header: %s\n", header.c_str());
  std::printf("claims: %s\n", claims.c_str());
}

La cabecera vuelve como {"alg":"HS256","typ":"JWT"} y los claims como {"sub":"1234567890","name":"John Doe","iat":1516239022} - el subject, el nombre y un timestamp de emisión. Esa es toda la parte de lectura, y es genuinamente útil: registrar lo que afirma un token, depurar un 401 mirando el campo de caducidad, o decidir qué claims confiar es todo a un solo decode de distancia. Lo que no es, es verificación. El segmento de firma también es base64url, y decodificarlo te da 32 o 64 bytes crudos que no prueban nada por sí solos; la firma solo tiene sentido cuando recalculas el hash de header.claims con el secreto compartido o la clave pública y comparas. Trata un JWT decodificado como tratarías una carta: dice lo que dice, y verificar el sello es un trabajo aparte, criptográfico.

Data URIs: archivos que se pegaron solos en una página

Un data URI es una URL cuyo payload está ahí mismo en la dirección: data: seguido de un media type opcional, un marcador opcional ;base64, una coma, y luego los datos en sí - todo el esquema del RFC 2397. Los navegadores los usan para incrustar imágenes, fuentes y scripts pequeños directamente en HTML y CSS sin peticiones extra, y si alguna vez ves una página que sigue funcionando con la red desactivada, un data URI es un sospechoso de peso. En el lado de C++, el trabajo de decodificación es dividir el URI y luego pasar el payload por tu decodificador de siempre, porque cuando el marcador ;base64 está presente el payload es base64 estándar plano - normalmente con padding, normalmente en una línea:

#include <cstddef>
#include <string>

std::string data_uri_payload(const std::string &uri, bool *is_base64) {
  const std::string prefix = "data:";
  if (uri.rfind(prefix, 0) != 0)
    return {};
  size_t comma = uri.find(',');
  if (comma == std::string::npos)
    return {};
  std::string meta = uri.substr(prefix.size(), comma - prefix.size());
  *is_base64 = meta.size() >= 7 &&
               meta.compare(meta.size() - 7, 7, ";base64") == 0;
  return uri.substr(comma + 1);
}

Llámalo con data:image/png;base64,iVBORw0KGgo=... y te entrega el payload más la bandera que te dice por qué ruta de decodificación pasar. Dos hoyos. Primero, cuando el marcador no está presente el payload es texto codificado por URL, no base64, así que la bandera no es una formalidad - un URI que parece base64 pero se generó como texto percent-encoded decodificará a basura. Segundo, algunos generadores envuelven data URIs largos con saltos de línea igual que el MIME; tu decodificador estricto los rechazará, así que quita los saltos de línea antes de decodificar si la fuente no está bajo tu control. Decodificar el payload de un data:image/png te da los bytes exactos del PNG, cabecera incluida, que es la satisfacción silenciosa de todo el ejercicio.

Email, MIME y el hábito de los 76 caracteres

Email es la razón por la que base64 aprendió a envolver sus líneas. SMTP, en su forma original, se construyó para llevar ASCII de siete bits, así que cualquier cosa binaria tenía que reescribirse como texto imprimible antes de poder viajar. Privacy-Enhanced Mail lo hizo en 1987 con líneas de 64 caracteres, y MIME, cuando estandarizó la codificación para email en 1993, relajó el límite a 76 caracteres y añadió la regla de que un decodificador conforme debe simplemente ignorar los saltos de línea. El hábito sobrevivió: un adjunto de email sigue siendo base64 hoy, envuelto a 76, y la aritmética exacta sale a 4/3 veces 78/76 - alrededor del 137 por ciento del tamaño original, más unos cientos de bytes de cabeceras. Tu decodificador de C++ lo encoge todo de vuelta al 100 por cien, que es el punto de todo el formato.

La complicación en C++ es que los decodificadores de este artículo no se ponen de acuerdo sobre los saltos de línea, y cada uno tiene su razón. El decodificador por streaming de OpenSSL los salta en cualquier parte del flujo, que es exactamente la regla de MIME. La función one-shot de OpenSSL se niega a cualquier espacio en blanco dentro del payload. Boost.Beast se detiene en el primer salto de línea sin decir nada. Los iteradores de Boost lanzan excepción ante un solo espacio. Así que cuando un payload viene de email, tu primera decisión es qué decodificador usar, o quitas los saltos de línea tú mismo - una pasada de una línea de erase-remove sobre \r y \n - y dejas que el decodificador que sea haga el trabajo real. Quitarlos de antemano es la opción aburrida y fiable, y es la que mantiene tu elección de decodificador independiente del historial de tu payload.

Bases de datos, archivos de configuración y variables de entorno

La tercera casa de base64 es la capa de almacenamiento: una columna en una base de datos heredada cuya documentación dice "base64" y nada más, un blob de configuración en un archivo JSON, un payload en una variable de entorno que un servicio hizo base64 para sobrevivir a un shell. El patrón de decodificación es el mismo que en todas partes - leer la cadena, decodificar, tratar como bytes - pero la entrada sin etiqueta merece un párrafo especial, porque a veces de verdad no sabes qué alfabeto se usó. No puedes saberlo, pero puedes probarlo, porque cuatro caracteres hacen casi todo el trabajo:

  • Contiene + o / - solo el alfabeto estándar puede ser el correcto.
  • Contiene - o _ - solo el alfabeto URL-safe puede ser el correcto.
  • Ninguno, pero termina en = - estándar con padding, o una cadena URL-safe con padding cuyo payload nunca llegó a necesitar los dos caracteres intercambiados.
  • Ninguno, sin padding - puede ser cualquiera; la forma URL-safe cruda es la común en la web, así que para datos nacidos en una URL o un token, prueba esa primero.

Las cadenas que no usan ninguno de los cuatro caracteres distintivos se decodifican igual bajo ambos alfabetos, así que para esas el orden en el que pruebes es cuestión de dónde salió el dato: las cosas nacidas en email quieren el alfabeto estándar, las nacidas en una URL quieren el de URL. Y recuerda probar la lectura con padding y sin padding de la misma cadena - un = que falta es la diferencia entre "rechazado" y "resuelto", que es por lo que el decodificador estricto de arriba acepta ambas.

La línea de comandos tiene dos base64

Para trabajos de una sola vez, una máquina Linux suele tener dos decodificadores base64, y se comportan de forma distinta de exactamente la manera que muerde a la gente. El primero es base64 de GNU coreutils (algunas distribuciones más nuevas traen en su lugar la reimplantación de uutils, y ambas hablan las mismas banderas - comprueba con base64 --version). Conforma con el RFC 4648, envuelve la salida a 76 caracteres al codificar (con -w 0 se apaga), y al decodificar acepta saltos de línea en cualquier parte sin inmutarse; su bandera -i hace explícita la tolerancia a la basura en lugar de accidental. El segundo es el de OpenSSL, y aquí va el giro: openssl base64 no es su propia app en absoluto. Desde la serie 1.1.0 (2016), el programa enc comprueba su propio nombre de invocación, y si se le llamó "base64" se conmuta él mismo a modo base64 - una comparación de cadenas sobre argv[0], que es la forma C de distribuir un alias. Sin -A, espera un salto de línea en alguna parte de los primeros 1024 bytes de entrada, así que una cadena larga de una sola línea vuelve vacía, con código de salida 0. Con -A lee una línea, y la lista de bugs documentada del comando enc es un museo de dos piezas: la opción -A no funciona correctamente con archivos grandes, y sin -A, si los primeros 1024 bytes no contienen ningún salto de línea, se ignoran las dos primeras líneas de entrada. En un pipeline, un archivo vacío en silencio parece exactamente un decode exitoso de un payload vacío.

# los one-liners honestos
base64 -d < payload.b64 > payload.bin
openssl base64 -d -A < payload.b64 > payload.bin

Ninguno habla base64url de forma nativa, que es una razón más para que el fragmento de transcode te entre en la memoria muscular. Para lo que importe, decodifica en tu programa, donde los errores vuelven como números que puedes comprobar y el código de salida de una herramienta silenciosa no es tu única señal.

Trampas que muerden específicamente a C++

  • El relleno de ceros. EVP_DecodeBlock devuelve tres bytes para TQ==: la letra M más dos ceros. Recupera la longitud real del padding, o usa la API por streaming, que es honesta con el recuento.
  • La rareza por streaming anterior a 3.5. En las versiones de OpenSSL anteriores a 3.5.0 (abril de 2025), EVP_DecodeUpdate tenía el mismo hábito de rellenar con ceros. El código escrito contra un pin de 3.0 o 3.3 puede estar mintiéndote sobre las longitudes de final; el arreglo está registrado en la sección de historia de la man page.
  • La parada en silencio. El decode de Boost.Beast no tiene canal de errores: se detiene en cualquier carácter no válido, cualquier salto de línea y cualquier longitud de final imposible, y devuelve un resultado parcial con total naturalidad. Comprueba que consumed + pads == input.size() y que el total es múltiplo de 4, o estás decodificando lo que a él se le ocurrió decodificar.
  • La trampa de decoded_size. b64::decoded_size(n) asume que n es divisible entre 4. Dos caracteres de entrada pueden producir un byte, mientras que decoded_size(2) dice cero - añade margen para longitudes raras.
  • Los pads de bytes a cero. Los iteradores de Boost decodifican = como el valor cero, así que TWFuZQ== se convierte en seis bytes incluyendo dos ceros finales. Resta el recuento de pads, o disfruta de tus fantasmas.
  • Espacio en blanco, de cuatro formas. El streaming de OpenSSL lo salta, el one-shot de OpenSSL lo rechaza internamente, los iteradores de archive lanzan excepción ante él, y Beast se detiene en él. Las cadenas pegadas adoran llevar espacio en blanco, y cada decodificador tiene su propia opinión al respecto.
  • Índice con char con signo. Si montas tu propia tabla de decodificación indexada por carácter, indexa con unsigned char. En plataformas donde char es con signo, un byte por encima de 127 se convierte en un índice negativo, que es comportamiento indefinido con bata de laboratorio.
  • El signo menos es un viajero del tiempo. En OpenSSL, - es un marcador suave de fin de entrada de la época de PEM, no un carácter del alfabeto. Transcódificalo antes de decodificar.
  • int, no size_t. Los parámetros de longitud de EVP son int. Por encima de 2 GB, solo la ruta por streaming por trozos es segura, que es la razón por la que existe.
  • Modo texto en Windows. Abrir un archivo para texto traduce CRLF a LF y corrompe tu entrada antes de decodificar. std::ios::binary, siempre, en todas las plataformas.
  • La most vexing parse. std::vector<char> v(istreambuf_iterator<char>(f), istreambuf_iterator<char>()) es una declaración de función. Usa inicialización entre llaves o un par de punteros.
  • Bits de padding no canónicos. Un decodificador tolerante puede aceptar cadenas cuyos bits de padding no usados no son cero, así que dos cadenas visiblemente distintas se decodifican a los mismos bytes (maleabilidad de base64). En los límites de seguridad, rechaza lo que no necesitas - el RFC 4648 dice que los decodificadores pueden hacer exactamente eso.
  • La línea de comandos falla en silencio. openssl base64 -d sin -A se traga la entrada de una sola línea (salida vacía, exit 0); los bugs documentados cubren archivos grandes y entrada sin saltos de línea en ambas direcciones. Revisa tu salida en los pipelines.
  • strlen sobre binario. std::string mantiene contentos a los bytes a cero, pero en el momento en que le pasas una cadena de C a una API heredada, strlen se detiene en el primer NUL. Pasa longitud y puntero, nunca un puntero a secas.

Una breve historia de Base64 en C++

El formato es más antiguo que la era moderna del lenguaje. El primer uso estandarizado de la codificación que hoy se llama base64 de MIME fue el protocolo Privacy-Enhanced Mail, propuesto en 1987 con líneas de 64 caracteres y una comprobación de integridad de mensaje RSA-MD2/MD5 pegada al final; el nombre "base64" en sí no llegó hasta 1993, cuando los estándares MIME le pusieron nombre. C++ llegó a la escena como C++98 en 1998 - cinco años después de MIME - y el primer código base64 al que acudieron los desarrolladores del lenguaje fue el par de funciones en C de Rene Nyffenegger (2004-2008), que una pregunta de Stack Overflow del 4 de diciembre de 2008 repartió por la web. La parte más bonita de esa historia es quién no se presentó: el autor nunca publicó una respuesta él mismo, pero su fragmento se convirtió en la canción de pueblo que todos copian. Una respuesta en el hilo repitió su implementación completa desde su propio sitio por si el sitio caía.

Entonces el ecosistema hizo lo que hacen los ecosistemas. En 2002, el Boost.Serialization de Robert Ramey sacó los adaptadores de iteradores - el base64 más antiguo de la caja de herramientas de C++, estricto hasta el punto de lanzar una excepción ante un solo espacio, un año antes de que el RFC 3548 codificara la regla que ya estaba aplicando. En 2017, Boost 1.66 trajo Beast, y con él el codec header-only que sigue distribuyéndose hoy con la atribución a Nyffenegger en su pie. Mientras tanto el estándar fue pasando por C++11, C++14, C++17, C++20 y C++23 (publicado en 2024), y cada uno de ellos miró el alfabeto de 64 caracteres y siguió de largo. C++26 añade una nueva cabecera <text_encoding> para el trabajo de codecs de texto, aprobada ya en 2022; su contenido técnico se terminó en la reunión ISO de marzo de 2026 en Londres, donde el comité votó 114-12-3 para enviarla a publicación, y las reuniones posteriores del comité en 2026 - incluyendo la de Búzios, Brasil, del 16 al 21 de noviembre - se dedican a abrir el siguiente borrador de trabajo, C++29, en vez de votar esta. Base64 nunca estuvo en el borrador. Siete estándares, tres décadas, una cabecera para codificación de texto - y el comité ha tenido ahora todas las excusas posibles para añadir base64 y se ha negado a todas. La historia práctica de base64 en C++ es, y sigue siendo, la historia de sus bibliotecas: las rutinas EVP de OpenSSL, dos sabores de Boost, una llamada a la API de Windows, y un fragmento de cuarenta líneas que es tuyo.

Datos curiosos, edición C++

  • El mismo par de funciones aparece en las respuestas a una pregunta de Stack Overflow de 2008, en el código fuente de Boost.Beast con un pie de atribución, y en las cabeceras de innumerables bases de código privadas. Pregunta a un desarrollador de C++ de dónde viene su base64 y la respuesta más honesta es "no lo sé, y no lo sabe ni internet".
  • Los iteradores de archive de Boost son el base64 más antiguo de este artículo, copyright 2002 - el mismo año que salió el SDK de .NET Framework 1.0. Lanzan una excepción ante un solo espacio, lo que significa que estaban aplicando la regla de "rechazar caracteres no alfabéticos" antes de que los RFC se pusieran al día: el RFC 3548 la codificó en 2003, y el RFC 4648 la repitió en 2006.
  • El decodificador por streaming de OpenSSL trabaja desde un buffer interno de 80 bytes, pero descarga cada 64 caracteres base64, el mismo ancho de línea que la armadura PEM usa desde 1987. Ese 64 silencioso es uno de los últimos sitios donde el formato antiguo sigue haciendo trabajo estructural en 2026.
  • El base64 con padding más pequeño posible son cuatro caracteres, TQ==: un byte con un disfraz de dos caracteres. El más pequeño sin padding son dos caracteres, TQ. Cuál de los dos te toca decodificar depende por completo de quién lo codificó, y esa persona no estaba pensando en ti.
  • La matemática de MIME es exacta: 4/3 veces 78/76, por eso un adjunto de email llega con alrededor del 137 por ciento de su tamaño original (más unos cientos de bytes de cabeceras por encima). Tu decodificador de C++ lo encoge de vuelta al 100 por cien, que es la alegría silenciosa de todo el ejercicio.
  • En un libstdc++ o MSVC típico, std::string lleva payloads pequeños en un buffer de pila mediante la small-string optimization en vez de reservar. Una entrada de 9 bytes decodifica a 6 bytes y nunca toca la heap. La forma base64 de tu blob de configuración diminuto puede vivir literalmente en un frame de pila, que es el tipo de comida gratis que la biblioteca estándar no publicita.
  • El comando openssl base64 al que puedes acudir en un shell no es un comando en absoluto. Es el programa enc comprobando su propio nombre en argv[0] y cambiando de personalidad. Un alias por comparación de cadenas, que es la forma C++ de hacer las cosas, en C.
  • Los identificadores de vídeo de YouTube son base64url: once caracteres, sin padding, sin + ni / por ninguna parte cerca de una URL. El formato de codificación más visto del planeta funciona sobre la variante "Segura para URL y Nombres de Archivo" que el RFC 4648 añadió en una sección que cabe en una página.

Cuando necesites empaquetar en su lugar

Todo lo que acabas de decodificar fue empaquetado en el otro lado por la misma caja de herramientas: EVP_EncodeBlock para one-shots, EVP_EncodeUpdate más EVP_EncodeFinal para streams (y de ahí vienen esas líneas de 64 caracteres), la misma matemática de buffers al revés, y el mismo impuesto del 33 por ciento que la decodificación devuelve en silencio. La historia completa del empaquetado - la matemática de tamaños itemizada, los codificadores que terminan su salida en NUL, el iterador de Boost que nunca ha conocido un carácter de padding, base64url, la envoltura MIME, archivos, y la API de Windows con su hábito CRLF - vive en la guía de codificación C++ del sitio hermanado. Ve a leerla, y luego vuelve y abre algo grande. Ese es todo el juego: sin biblioteca estándar, tres proveedores de confianza con tres temperamentos distintos, un decodificador que señala el byte exacto que dolió, un bugfix de 2025 que cambió el final del streaming, y un trío relleno de ceros para recordar para siempre. Feliz desempaque.

Última actualización: 2026-09-08

Artículo relacionado: Codificación Base64 en C++ (Cpp): una guía completa