Codificación Base64 en C++ (Cpp): una guía completa
El problema inverso es el que tiene el titular más grande: tienes bytes - un certificado, una imagen, un blob aleatorio, una firma - y necesitan viajar a través de algo que solo habla texto: un campo JSON, una cabecera de email, una URL, una variable de entorno. La página de inicio de este sitio repasa el formato en profundidad, así que esto es solo la versión corta: tres bytes se convierten en cuatro caracteres del alfabeto, un final corto recibe una o dos marcas =, y la forma codificada es aproximadamente un 33 por ciento más grande que el original. Codificar es la dirección que crece, así que cada buffer de este artículo está dimensionado para eso, y la aritmética es un one-liner - 4 * ((n + 2) / 3) - que no cambia sin importar qué codificador elijas.
Como en el lado de la decodificación, C++ en sí no codificará ni un solo byte 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 codificador de entre un banquillo de cuatro personalidades muy distintas, más la opción de escribir unas cuarenta líneas propias. Una es un caballo de batalla que ha llevado el TLS desde los 90 y que añade padding, termina en NUL y envuelve líneas sin pedir permiso. Una es un codec header-only rápido escondido en un espacio de nombres que sus autores etiquetaron como "detail". Una es un iterador de 2002 que aparentemente nunca ha conocido un carácter de padding. Una es una función que el sistema operativo lleva distribuyendo décadas y que añade CRLF al final de tu token. Y la quinta opción es la tuya. Cuando sepas lo que cada una añade, rechaza o adjunta en silencio, codificar deja de ser una fuente de bugs de off-by-one. Vamos al empaquetado.
El estándar nunca ha incluido un empaquetador
Todos los estándares desde C++98 - y han sido ocho, hasta C++26 - han mirado el alfabeto de 64 caracteres y seguido de largo. No hay <base64>, no hay std::base64, nada en <string> ni en <vector> que empaquete tus bytes. El trabajo técnico de C++26 se terminó y fue votado a favor (114-12-3) en la reunión ISO de C++ de marzo de 2026 en Croydon, Reino Unido, y sí añade una cabecera <text_encoding> para el trabajo de codecs de texto; las próximas reuniones del comité, en junio de 2026 (Brno) y noviembre de 2026 (Búzios, Brasil), abren el borrador de trabajo de C++29 en vez de revisar C++26. Base64 no está en el estándar, y es difícil culpar al comité: la codificación de texto va de conjuntos de caracteres, y base64 va de bytes, así que la nueva cabecera nunca fue la casa adecuada. En la práctica, el ecosistema hizo el trabajo. Las rutinas base64 de EVP de OpenSSL han estado en cada versión de OpenSSL, las bibliotecas de Boost llevan dos codificadores independientes, Windows distribuye una función de CryptoAPI con una tabla de banderas para el trabajo, y un fragmento de cuarenta líneas ha sido copiado y pegado por todo el lenguaje desde 2008. Si tu proyecto se basa en CMake, toda la configuración de dependencias son tres líneas:
find_package(OpenSSL REQUIRED)
find_package(Boost REQUIRED)
target_link_libraries(my_app PRIVATE OpenSSL::Crypto)
La versión de Boost a tener en cuenta es la 1.92.0, de agosto de 2026, de un proyecto fundado en 1998 que lleva distribuyendo bibliotecas desde su primera versión en 1999. Ambos codificadores de Boost de más abajo son header-only - no hay nada que enlazar - mientras que OpenSSL quiere -lcrypto, que la mayoría de los programas en C++ que rozan TLS ya tienen en el binario.
Primero, las matemáticas: dimensionar cada buffer de este artículo
Base64 agrupa los bytes de tres en tres, así que la longitud de la salida tiene una forma que nunca sorprende una vez que la conoces: por cada 3 bytes de entrada, 4 caracteres de salida, y un final corto se rellena hasta un grupo completo. El recuento exacto para n bytes de entrada es:
4 * ((n + 2) / 3)
El +2 es el truco del techo: la división entera redondea hacia abajo, así que añadir 2 primero hace que redondee hacia arriba al siguiente múltiplo de tres. A partir de ahí, cada tamaño de buffer de este artículo es una sustitución. La función one-shot de OpenSSL quiere un buffer que pueda contener los datos codificados más el NUL que añade al final - la man page ilustra el contrato con 16 bytes de entrada que se convierten en 24 bytes codificados más 1 NUL, 25 bytes en total, y la función devuelve la longitud sin el NUL. Su ruta por streaming procesa la entrada en bloques de 48 bytes, y la man page dimensiona la salida en 65 bytes por bloque (64 caracteres más el salto de línea que cada bloque produce siempre) con un byte más para el NUL. La cabecera de Boost.Beast te entrega la fórmula exacta como función constexpr. Y tu propio código reserva (n + 2) / 3 * 4 y ya está. Estos son los números que vas a encontrarte de verdad:
| Entrada | Salida (con padding) | Qué notar |
|---|---|---|
| 1 byte | 4 caracteres | La forma con padding más pequeña: QQ== |
| 2 bytes | 4 caracteres | Tres caracteres de datos y un pad |
| 3 bytes | 4 caracteres | Un grupo completo, sin ningún padding |
| 48 bytes | 64 caracteres | Exactamente un bloque por streaming de OpenSSL |
| 500 bytes | 668 caracteres | Envuelto a 64 son 11 líneas, 679 caracteres con saltos de línea |
| 1 GB | unos 1,33 GB | Presupuesta la columna, el archivo y el cable para el impuesto |
Si el lado receptor es una columna de tamaño fijo, un buffer, o una línea en un archivo de texto, esta fórmula es el documento de diseño entero. La única dirección en la que puede morder es la otra: el lado de la decodificación necesita 3n/4 menos pads, y un buffer de decodificación dimensionado con la fórmula de codificación es una sobre-reserva clásica que crece hasta convertirse en un ticket de bug de memoria. Dimensionar la dirección que encoge es el problema de la guía hermana; aquí solo creces.
Aquí tienes el panorama, porque las diferencias están todas en los extras - el padding, los saltos de línea, los NUL - y no en el empaquetado central, que cada fila implementa de forma idéntica:
| Codificador | De dónde viene | Padding | Bytes extra a presupuestar | Particularidad a recordar |
|---|---|---|---|---|
EVP_EncodeBlock |
<openssl/evp.h>, enlaza -lcrypto |
Siempre | 1 (un NUL en el buffer) | El ejemplo de 16 bytes de la man page es el contrato |
EVP_EncodeUpdate + Final |
lo mismo | Siempre | 65 por bloque de 48 bytes | Dobra de forma fija a 64 caracteres, cada bloque termina en un salto de línea |
Boost.Beast encode |
boost/beast/core/detail/base64.hpp, header-only |
Siempre | 0 | Vive en un espacio de nombres llamado detail |
| Iteradores de Boost.Serialization | boost/archive/iterators/base64_from_binary.hpp, header-only |
Nunca | 0 - tú añades los 1 o 2 pads | El codificador más antiguo de la caja de herramientas, 2002 |
CryptBinaryToStringA |
wincrypt.h, crypt32.lib |
Siempre | 2 (un CRLF) a menos que NOCRLF |
Tiene una bandera URL-safe que el resto de la caja no tiene |
| Tus propias cuarenta líneas | En ninguna parte: son tuyas | A tu elección | A tu elección | Tú eres el dueño de cada caso límite para siempre |
El algoritmo central es idéntico en cada fila - esa es la parte reconfortante de un formato de 1987. Lo que difiere es lo que cada implementación añade alrededor del payload, y casi cada trampa de este artículo es una de esas adiciones encontrando a un consumidor que no se la esperaba.
OpenSSL: el codificador que tu pila TLS ya enlaza
Si tu programa ya enlaza OpenSSL para TLS, no necesitas añadir nada. La función one-shot es una sola llamada:
int EVP_EncodeBlock(unsigned char *t, const unsigned char *f, int n);
Le pasas los bytes de origen y la longitud, y escribe la codificación con padding en una sola línea. El contrato merece la pena memorizarlo, porque la man page lo declara con un ejemplo: por cada 3 bytes de entrada, 4 bytes de salida; un final que no es divisible entre 3 se rellena para que la salida sea siempre divisible entre 4; y encima se añade un carácter NUL terminador. El ejemplo documentado son 16 bytes de entrada, 24 bytes codificados más 1 NUL, 25 bytes en total en el buffer, y la función devuelve 24 - la longitud sin el NUL. Dimensiona el buffer en consecuencia y el wrapper son un par de líneas:
#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>
std::string openssl_encode(const std::string &in) {
std::string out;
out.resize(4 * ((in.size() + 2) / 3) + 1);
int n = EVP_EncodeBlock(reinterpret_cast<unsigned char *>(out.data()),
reinterpret_cast<const unsigned char *>(in.data()),
static_cast<int>(in.size()));
if (n < 0) return {};
out.resize(static_cast<size_t>(n));
return out;
}
int main() {
std::printf("%s\n", openssl_encode("Mane").c_str());
std::printf("%s\n", openssl_encode("M").c_str());
std::printf("%s\n", openssl_encode("").c_str());
}
Fíjate en lo que hace el std::string que C te obligaría a hacer: crece exactamente hasta la longitud devuelta, así que el NUL que añadió OpenSSL queda simplemente más allá de la longitud registrada y nunca se convierte en parte del payload. Codifica "Mane" y obtienes TWFuZQ==, el final clásico de cuatro caracteres con su único pad; codifica un byte y obtienes un par de datos de dos caracteres con un disfraz de padding de dos caracteres; no codifiques nada y obtienes la cadena vacía, el único caso donde un codificador base64 se comporta exactamente como la función identidad. La única línea de lógica real de toda la función es el resize: convierte "bytes escritos más un NUL" en "exactamente el payload".
Para datos que llegan en trozos - un archivo, un socket, un flujo que no quieres poner en buffer - OpenSSL tiene un contexto al que alimentas y finalizas, y la aritmética de bloques de la man page es inusualmente explícita. Solo los bloques completos de 48 bytes se procesan de inmediato; cualquier resto se retiene dentro del contexto y lo libera una llamada posterior o la final. Cada bloque procesado escribe 64 caracteres más un salto de línea - 65 bytes - y la llamada final maneja el bloque parcial, por eso su tope documentado son 65 bytes más el NUL. La consecuencia a conocer antes de llamar: esta API envuelve a los 64 caracteres. No es configurable. Eso es lo que es el codificador por streaming.
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
#include <openssl/evp.h>
std::string openssl_encode_wrapped(const std::string &in) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_EncodeInit(ctx);
std::string out;
out.reserve(4 * ((in.size() + 2) / 3) + in.size() / 48 + 2);
std::vector<unsigned char> buf(128);
int outl = 0;
for (size_t pos = 0; pos < in.size();) {
size_t take = std::min<size_t>(48, in.size() - pos);
EVP_EncodeUpdate(ctx, buf.data(), &outl,
reinterpret_cast<const unsigned char *>(in.data()) + pos,
static_cast<int>(take));
out.append(reinterpret_cast<const char *>(buf.data()), outl);
pos += take;
}
EVP_EncodeFinal(ctx, buf.data(), &outl);
out.append(reinterpret_cast<const char *>(buf.data()), outl);
EVP_ENCODE_CTX_free(ctx);
return out;
}
int main() {
std::string s = openssl_encode_wrapped(std::string(500, 'A'));
std::printf("500 bytes -> %zu chars\n", s.size());
int lines = 0;
size_t longest = 0, run = 0;
for (char c : s) {
if (c == '\n') { lines++; run = 0; }
else run++;
longest = std::max(longest, run);
}
std::printf("lines=%d longest=%zu lastchar=%c\n", lines, longest, s.back());
}
Aliméntalo con 500 bytes de la letra A y la contabilidad sale exactamente como la man page prometió: 668 caracteres codificados, y como la salida se corta en líneas de 64 caracteres, obtienes 11 líneas, 679 caracteres en total, y el carácter del final absoluto es un salto de línea. Ese salto de línea final es el que rompe a los consumidores: pega el resultado en una cadena JSON y tienes un carácter de control donde debía haber una comilla; úsalo como segmento de token y te has inventado un segmento nuevo. La regla práctica: la API de bloques para payloads de una línea (tokens, cabeceras, valores de configuración), la API por streaming cuando el consumidor quiere una salida envuelta con forma MIME, y cuando dudes, quita el salto de línea final con un bucle while (out.back() == '\n') antes de que el payload cruce un límite que no lo espera.
Boost.Beast: un empaquetador rápido en un espacio de nombres detail::
La biblioteca HTTP de Boost 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 promover el codec a API pública. Todo el mundo lo usa de todas formas: es pequeño, es rápido, es header-only (define BOOST_BEAST_HEADER_ONLY antes del include y no hay nada que enlazar), y es el mismo codec que el propio handshake de WebSocket de Boost.Beast usa para el cómputo de Sec-WebSocket-Accept, lo que significa que lleva años masticando tráfico real.
En el lado de la codificación la API es casi insultantemente calmada. Un helper constexpr te da el tamaño exacto de la salida - 4 * ((n + 2) / 3), la misma fórmula que la sección de matemáticas, ahora con un compilador que la comprueba - y la función encode escribe el resultado con padding en tu buffer y te dice cuántos caracteres usó. No hay canal de errores, porque codificar no puede fallar: cualquier byte es entrada válida, y la longitud de la salida es una función pura de la longitud de la entrada. El wrapper:
#define BOOST_BEAST_HEADER_ONLY
#include <boost/beast/core/detail/base64.hpp>
#include <cstddef>
#include <cstdio>
#include <string>
namespace b64 = boost::beast::detail::base64;
std::string beast_encode(const std::string &in) {
std::string out(b64::encoded_size(in.size()), '\0');
std::size_t n = b64::encode(out.data(), in.data(), in.size());
out.resize(n);
return out;
}
int main() {
std::printf("%s\n", beast_encode("Mane").c_str());
std::printf("%s\n", beast_encode("M").c_str());
}
Codifica "Mane" y obtienes TWFuZQ==; codifica el solo byte M y obtienes TQ== - los mismos bytes que produjo el wrapper de OpenSSL, sin ningún NUL de qué preocuparse y sin líneas que quitar. Dos cosas para archivar. Primero, la procedencia: el código fuente tiene copyright 2016-2019 de Vinnie Falco, con un pie de página que atribuye partes a un fragmento de Rene Nyffenegger de 2004-2008 - la misma canción de pueblo que empezó la historia de base64 en C++, ahora distribuyéndose dentro de Boost, en tu binario, haciendo handshakes de WebSocket para toda la web. Segundo, la práctica: porque el codec añade padding y nunca envuelve, es la herramienta correcta para cualquier cosa que tenga que ser una línea - tokens, cabeceras, payloads de API - y la fórmula encoded_size te da un buffer que es exactamente correcto, nunca una aproximación.
Boost.Serialization: el iterador que olvidó que el padding existe
El base64 más antiguo del ecosistema de C++ no es una función sino un conjunto de adaptadores de iteradores componibles, escritos por Robert Ramey en 2002 para la biblioteca de serialización de Boost. La dirección de codificación es una cadena de dos adaptadores: un transformador de anchura que reagrupa tus bytes crudos de ocho a seis, y un iterador que convierte cada valor reagrupado en un carácter del alfabeto:
#include <boost/archive/iterators/base64_from_binary.hpp>
#include <boost/archive/iterators/transform_width.hpp>
#include <cstddef>
#include <cstdio>
#include <string>
namespace it = boost::archive::iterators;
std::string boost_iter_encode(const std::string &in) {
using enc =
it::base64_from_binary<it::transform_width<const char *, 6, 8>>;
std::string out(enc(in.data()), enc(in.data() + in.size()));
switch (in.size() % 3) {
case 1: out += "=="; break;
case 2: out += '='; break;
default: break;
}
return out;
}
int main() {
std::printf("%s\n", boost_iter_encode("Mane").c_str());
std::printf("%s\n", boost_iter_encode("M").c_str());
}
El iterador hace el empaquetado central y nada más - sin padding, sin NUL, sin saltos de línea y sin canal de errores, porque el empaquetado central no puede fallar. Codifica "Mane" y el iterador te entrega seis caracteres, TWFuZQ, con total naturalidad: una codificación real de cuatro bytes son ocho caracteres, y a un iterador de 2002 no le importó jamás. Por eso la sentencia switch es estructural, no decorativa: un byte menos que un grupo recibe dos pads, dos bytes menos recibe uno. La misma cadena menos el switch es lo que obtienes si olvidas ese paso, y el resultado es una cadena que se decodifica bien bajo un decodificador tolerante, falla bajo uno estricto, y convierte el mensaje de error de tu consumidor de API en un misterio. (El lado de decodificación de esta misma familia de iteradores es el que lanza una excepción ante un solo espacio suelto - más en la guía hermana.)
Cuarenta líneas, cero dependencias
Base64 es lo bastante pequeño para que un codificador 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, la fórmula te da el tamaño exacto de antemano, y un codificador hecho a mano es el que no tiene opiniones en absoluto - sin NUL, sin saltos de línea, sin hábitos de plataforma - que es exactamente lo que quieres bajo un archivo de configuración o un límite de API. Esta versión empaqueta en grupos de 3 bytes contra una tabla de 64 caracteres:
#include <cstddef>
#include <cstdio>
#include <string>
std::string base64_encode(const std::string &in) {
static const char *table =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
std::string out;
out.reserve((in.size() + 2) / 3 * 4);
const unsigned char *p =
reinterpret_cast<const unsigned char *>(in.data());
size_t n = in.size();
for (size_t i = 0; i < n; i += 3) {
unsigned v = p[i] << 16;
if (i + 1 < n) v |= p[i + 1] << 8;
if (i + 2 < n) v |= p[i + 2];
out.push_back(table[(v >> 18) & 63]);
out.push_back(table[(v >> 12) & 63]);
out.push_back(i + 1 < n ? table[(v >> 6) & 63] : '=');
out.push_back(i + 2 < n ? table[v & 63] : '=');
}
return out;
}
int main() {
std::printf("%s\n", base64_encode("Mane").c_str());
std::printf("%s\n", base64_encode("M").c_str());
std::printf("%s\n", base64_encode("M\312\277").c_str());
}
Repasemos las piezas. La línea de reserve es la sección de matemáticas: (n + 2) / 3 * 4 caracteres, exactamente, así que no hay reasignación a mitad del bucle. El reinterpret_cast a const unsigned char * no es ceremonia - en plataformas donde char es con signo, un byte por encima de 127 sería de otra forma un número negativo, y en el momento en que tocara un índice de tabla tendrías comportamiento indefinido con bata de laboratorio. Cada iteración mete hasta tres bytes en un valor de 24 bits, empuja las cuatro rebanadas de 6 bits a la tabla, y en el final emite = en lugar del byte que no estaba ahí - las guardas i + 1 < n y i + 2 < n son toda la lógica de padding. Aliméntalo con "Mane" y obtienes TWFuZQ==. Aliméntalo con una sola M y obtienes TQ==. Aliméntalo con bytes por encima de 127 - el par 0xCA 0xBF de la tercera línea del ejemplo - y la salida sigue siendo ASCII puro (Tcq/), porque un byte por encima de 127 es simplemente un byte, y a la tabla no le importa lo que signifique. Cuarenta líneas, sin dependencias, y cada caso límite es una línea que escribiste tú, que es todo el punto.
CryptoAPI de Windows: el empaquetador integrado en el SO
En Windows hay un codificador base64 en el propio sistema operativo, más antiguo que la mayoría de los frameworks de este artículo: CryptBinaryToStringA de wincrypt.h, en crypt32.lib, parte de la CryptoAPI que se distribuye con Windows desde hace décadas. Convierte una matriz de bytes en una cadena con formato, y su tabla de banderas se lee como un menú de toda la historia del formato:
| Bandera | Valor | Lo que obtienes |
|---|---|---|
CRYPT_STRING_BASE64HEADER |
0x0 |
Base64 envuelto en las líneas de cabecera BEGIN/END de certificado |
CRYPT_STRING_BASE64 |
0x1 |
Base64 plano, sin cabeceras |
CRYPT_STRING_BASE64URI |
0xD |
El alfabeto URL-safe: + se convierte en -, / se convierte en _, según la sección 5 del RFC 4648 |
CRYPT_STRING_NOCRLF |
0x40000000 |
No se añade ningún salto de línea al final |
CRYPT_STRING_NOCR |
0x80000000 |
Un LF a secas en lugar del CRLF por defecto |
Lo primero a saber es el valor por defecto: a menos que pases CRYPT_STRING_NOCRLF, la función añade una pareja retorno de carro/salto de línea al final de tu cadena - el comportamiento documentado es que cada formato no binario recibe una secuencia de salto de línea - así que un token base64 que debe caber en una línea quiere BASE64 | NOCRLF, y esa combinación es la llamada idiomática. Lo segundo es la convención de llamada, que es el clásico paso doble de Windows: llamar con un buffer NULL para preguntar cuánto espacio se necesita (la respuesta incluye el NUL terminador), reservar, llamar de nuevo, y leer la longitud sin el NUL:
#include <windows.h>
#include <wincrypt.h>
#include <cstddef>
#include <string>
std::string win32_encode(const std::string &in,
DWORD flags = CRYPT_STRING_BASE64) {
DWORD need = 0;
if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
static_cast<DWORD>(in.size()),
flags | CRYPT_STRING_NOCRLF,
nullptr, &need))
return {};
std::string out(need, '\0');
DWORD got = 0;
if (!CryptBinaryToStringA(reinterpret_cast<const BYTE *>(in.data()),
static_cast<DWORD>(in.size()),
flags | CRYPT_STRING_NOCRLF,
out.data(), &got))
return {};
out.resize(got);
return out;
}
Dos notas más. La bandera URI es el único base64url nativo de todo este artículo - en Windows puedes codificar el alfabeto de token directamente, y el enfoque de transcode de la sección de más abajo es estrictamente para las otras plataformas. Y la entrada CRYPT_STRING_BASE64HEADER, con su valor de 0, es también la bandera que obtienes si pasas cero, así que una llamada que "quería decir" ninguna bandera en absoluto envuelve en silencio el payload en las líneas de cabecera de certificado - el hábito de la época de PEM de enmarcar, útil para generar archivos .pem y una sorpresa para todo lo demás. Enlaza contra crypt32.lib y la función es tuya para el resto de la vida del programa.
Base64url: el alfabeto para tokens y URLs
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 los pads serían solo percent-escapes a la espera de pasar.
De los codificadores de este artículo, solo la bandera de Windows emite el alfabeto de forma nativa - OpenSSL no tiene modo URL-safe, y ni el uno ni el otro sabor de Boost lo tienen - así que en la mayoría de plataformas la receta es: codificar en estándar, intercambiar los dos caracteres, soltar los pads. Son una docena de líneas:
#include <cstddef>
#include <cstdio>
#include <string>
/* base64_encode de la sección "Cuarenta líneas, cero dependencias" */
std::string base64url_encode(const std::string &in, bool pad = false) {
std::string out = base64_encode(in);
for (char &c : out) {
if (c == '+') c = '-';
else if (c == '/') c = '_';
}
if (!pad)
while (!out.empty() && out.back() == '=')
out.pop_back();
return out;
}
int main() {
std::printf("%s\n", base64url_encode("M\312\277").c_str());
std::printf("%s\n", base64url_encode("M").c_str());
std::printf("%s\n", base64url_encode("M", true).c_str());
}
La primera línea de salida es Tcq_, donde el alfabeto estándar habría escrito /; las otras dos líneas muestran el interruptor de padding en acción - TQ sin padding por defecto, TQ== cuando el consumidor lo quiere de vuelta. Ese argumento pad es el que hay que pensar, porque los consumidores no se ponen de acuerdo: los segmentos de JWT no quieren pads, los challenges de PKCE no quieren pads, pero un valor base64url que acaba en un campo donde el decodificador es estricto con la longitud puede quererlos de vuelta, y el interruptor es un bool, no una reescritura. Y el modo de fallo a recordar en la dirección opuesta: una - dentro de un payload de alfabeto estándar es simplemente inválida, así que los dos alfabetos no son intercambiables a nivel de byte - un token codificado con el alfabeto equivocado no se decodifica, falla, que es el fallo que quieres en un límite de seguridad.
Envoltura de líneas: 64, 76, o nunca
El base64 envuelto tiene tres longitudes de línea en el mundo salvaje, cada una con su historia. El codificador por streaming de OpenSSL es duro a 64 caracteres - el hábito de PEM, donde el estándar de 1987 de Privacy-Enhanced Mail envolvía a 64. MIME, cuando estandarizó la codificación para email en 1993, pasó a 76 caracteres, y ese número es el valor por defecto del comando base64 de coreutils (su bandera -w fija el ancho, y -w 0 apaga la envoltura por completo) y de la mayoría de las herramientas del ecosistema. El RFC 4648 en sí no se pronuncia: cita 76 como el límite de MIME y dice a las implementaciones que no envuelvan en absoluto a menos que la especificación que las referencia lo ordene. Cuál emites depende de quién lo consume, y el consumidor - no el formato - es la restricción de diseño.
La envoltura es un paso de post-procesado sobre la cadena codificada, nunca un paso de entrada: los grupos de 4 caracteres son la unidad de significado, así que cortar la cadena en cualquier múltiplo del ancho es un corte seguro - cada límite de línea cae entre grupos. La versión en C++ es un bucle:
#include <cstddef>
#include <cstdio>
#include <string>
/* base64_encode de la sección "Cuarenta líneas, cero dependencias" */
std::string wrap_lines(std::string s, size_t width = 76) {
std::string out;
for (size_t i = 0; i < s.size(); i += width)
out += s.substr(i, width) + "\r\n";
return out;
}
int main() {
std::string mime = wrap_lines(base64_encode(std::string(200, 'x')));
int lines = 0;
for (char c : mime)
if (c == '\n') lines++;
std::printf("mime: %d lines, %zu chars\n", lines, mime.size());
}
La contabilidad: 200 bytes codifican a 268 caracteres, y envuelto a 76 con terminadores CRLF son 4 líneas - tres líneas completas y un final de 40 caracteres - 276 caracteres en el cable. La elección de CRLF del fragmento es la elección de email; para todo lo demás, LF es el valor por defecto moderno, y la única regla que no es negociable es la consistencia - un decodificador que espera CRLF leerá un LF suelto como un carácter de datos si va en serio. (La regla de MIME es que los decodificadores deben ignorar los saltos de línea, que es la razón por la que el email nunca ha sufrido la diferencia.) El tercer hábito a conocer: el comando openssl base64 - que es el programa enc con gabardina, comprobando su propio nombre en argv[0] - envuelve a 64 sin -A y emite una línea con -A, y es la única herramienta de la línea de comandos cuyo comportamiento compruebas en cada ejecución en vez de fiarte de la memoria.
Binario en JSON y configuración
Una cadena JSON tiene una lista pequeña de caracteres que no puede contener en bruto: la comilla, la barra invertida, y los caracteres de control por debajo de 0x20. Un certificado, una clave aleatoria, una firma - todos ellos están llenos de bytes que se convertirían en una cascada de escapes si intentaran viajar dentro de un campo de cadena en bruto, y los caracteres de control harían que algunos parsers se atragantasen en seco. Base64 es la solución, y es la respuesta por defecto que da todo formato de configuración que debe llevar binario: el valor se guarda como una sola línea de caracteres puros del alfabeto, y las reglas de comillas de la biblioteca JSON no tienen nada más que hacer.
El patrón de C++ es toda la implementación: leer los bytes (en modo binario, obviamente), codificar, guardar la cadena. El consumidor decodifica en el otro lado. La trampa específica de JSON es la cadena envuelta: un certificado envuelto a 76 caracteres pegado tal cual en un archivo JSON es una cadena llena de caracteres de control literales, que es un error de parseo o una corrupción silenciosa, según el humor del parser. Si el valor debe estar envuelto para los ojos humanos, debe estar escapado o debe ser una línea - y para configuración máquina a máquina, una línea es la respuesta. La otra trampa es el valor sin etiqueta: una columna de configuración que dice base64 en una man page de 2014 suele ser alfabeto estándar con padding, pero los tokens de la era de las API son URL-safe sin padding, y la prueba de cuatro caracteres de la guía de decodificación - ¿contiene + o /, - o _, un = al final? - es todo el diagnóstico.
Data URIs: archivos que se pegan en páginas
Un data URI es una URL cuyo payload está ahí mismo en la dirección: data:, un media type opcional, un marcador opcional ;base64, una coma, y 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 una página sigue funcionando con la red desactivada, un data URI es un sospechoso de peso. En el lado de C++, el trabajo de codificación es ensamblar la cadena, que es una concatenación de cadenas con una constante:
#include <cstdio>
#include <string>
/* base64_encode de la sección "Cuarenta líneas, cero dependencias" */
std::string make_data_uri(const std::string &mime_type,
const std::string &binary) {
return "data:" + mime_type + ";base64," + base64_encode(binary);
}
int main() {
std::printf("%s\n", make_data_uri("text/plain", "hi").c_str());
}
Las trampas están todas en los detalles. El marcador ;base64 son exactamente 7 caracteres, que es la longitud a la que van a por ti los bugs de off-by-one: un parser que comprueba 6 es un parser que acepta data:text/plain;base4,... y decodifica basura con total naturalidad. Y el payload de un data URI base64 es una línea - los saltos de línea no son parte de la gramática del URI, así que si tu codificador envolvió la imagen a 76 (los codificadores con forma MIME lo hacen, por defecto), el URI está roto antes de llegar al navegador. La regla para este consumidor: codificar, no envolver, y mantener el media type exacto - un image/png equivocado sobre un JPEG es el tipo de mentira que solo aparece como una miniatura rota a las 2 de la mañana.
Tokens: JWTs, PKCE y claves de API
El base64 con más en juego en internet está en los tokens. Un JSON Web Token es tres segmentos base64url pegados con puntos: un JSON de cabecera, un JSON de claims, y una firma computada sobre la cadena header.claims. C++ no tiene un tipo JWT incorporado, pero construir uno es el codificador base64url de más arriba más una llamada HMAC, porque el token entero es base64url hasta que deja de serlo - hasta que es una firma:
#include <cstddef>
#include <cstdio>
#include <string>
#include <openssl/evp.h>
#include <openssl/hmac.h>
/* base64_encode y base64url_encode de secciones anteriores */
std::string jwt_hmac256(const std::string &signing_input,
const std::string &secret) {
unsigned char digest[EVP_MAX_MD_SIZE];
unsigned int len = 0;
HMAC(EVP_sha256(), secret.data(), static_cast<int>(secret.size()),
reinterpret_cast<const unsigned char *>(signing_input.data()),
signing_input.size(), digest, &len);
return std::string(reinterpret_cast<const char *>(digest), len);
}
int main() {
const std::string header_json = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
const std::string claims_json =
"{\"sub\":\"1234567890\",\"name\":\"John Doe\",\"iat\":1516239022}";
std::string head = base64url_encode(header_json);
std::string claims = base64url_encode(claims_json);
std::string signing_input = head + "." + claims;
std::string sig = base64url_encode(jwt_hmac256(signing_input, "secret"));
std::printf("token: %s\n", (signing_input + "." + sig).c_str());
}
Ejecuta el ejemplo y el token que sale es un token HS256 de manual: la cabecera decodifica a {"alg":"HS256","typ":"JWT"}, los claims a un subject, un nombre y un timestamp de emisión, y la firma es el base64url de un HMAC-SHA256 sobre los dos segmentos codificados. Tres detalles cargan todo el diseño. La entrada de firma son los segmentos codificados, no el JSON en bruto - firma el JSON y habrás firmado los bytes equivocados. Los segmentos son base64url sin padding - los pads estarían en mitad de una URL, y todo el punto del alfabeto era mantener el token como una sola cadena limpia. Y HS256 significa un secreto compartido, que es un algoritmo de servidor a servidor: un secreto que vive en el código de un cliente no es un secreto, y el token que firma no es una credencial. (El flujo PKCE de OAuth usa el mismo alfabeto a un paso: un verifier aleatorio, hasheado con SHA-256, base64url sin pads en un code challenge - el codificador de la sección de base64url es toda la implementación del lado del cliente.)
Basic auth en HTTP
El base64 más antiguo de HTTP es la cabecera de credenciales: Authorization: Basic seguido del base64 de user:password, un esquema tan viejo que precede a JSON. Construirlo es una concatenación:
#include <cstdio>
#include <string>
/* base64_encode de la sección "Cuarenta líneas, cero dependencias" */
std::string basic_auth_header(const std::string &user,
const std::string &pass) {
return "Basic " + base64_encode(user + ":" + pass);
}
int main() {
std::printf("%s\n", basic_auth_header("user", "password").c_str());
}
La salida es la cadena que probablemente hayas visto en una petición capturada: Basic dXNlcjpwYXNzd29yZA==. Dos notas de C++. La concatenación user + ":" + pass es donde una contraseña que contiene un signo de dos puntos confundiría a un parser ingenuo del otro lado - la regla de parseo es "dividir en el primer signo de dos puntos", que es por lo que el lado que construye es libre de poner cualquier cosa en cualquiera de los dos campos. Y si las credenciales no son ASCII, la lectura segura del esquema es tratar el user-id y la contraseña como UTF-8 antes del base64, que en C++ significa que tu std::string ya está haciendo el trabajo - siempre que lo hayas llenado con bytes UTF-8 y no con lo que la locale decidió. La nota de seguridad va con cada mención de este esquema: 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. (El codec de Boost.Beast - el del espacio de nombres detail:: - hace este mismo trabajo de cabecera dentro de la implementación de WebSocket de Boost, poniendo en base64 el digest SHA-1 que se convierte en la clave Sec-WebSocket-Accept, que es la evidencia silenciosa de que el patrón lleva haciendo esto desde 2017.)
Email: reglas de siete bits, respuesta Base64
Email es donde base64 aprendió sus hábitos, y los hábitos siguen siendo estructurales. 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 una comprobación de integridad de mensaje RSA-MD2/MD5 pegada al final, 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. 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 814 bytes de cabeceras.
El lado de C++ es el codificador más la función de envoltura de más arriba - codificar a una línea, envolver a 76 con CRLF, listo. Los dos detalles específicos de email: la línea final puede o no llevar un salto de línea al final (los decodificadores están obligados a ignorarlo, así que cualquiera es legal y ambos son comunes), y el valor envuelto no es un valor JSON, una variable de entorno, ni un token - es un blob que pertenece a un cuerpo MIME, y moverlo a cualquier otra parte es donde la envoltura deja de ser un hábito y empieza a ser un bug. La dirección inversa - un adjunto que llega envuelto a 76 - es territorio de la guía hermana, donde los cuatro decodificadores de C++ no se ponen de acuerdo sobre los saltos de línea de cuatro formas distintas.
Archivos, flujos y el techo de dos gigabytes
Codificar un archivo es la imagen especular del trabajo con archivos de la guía de decodificación: abrir en modo binario (en Windows una lectura en modo texto traduciría las parejas CRLF a saltos de línea simples y cambiaría tus datos antes de que el codificador los viera), leer los bytes, codificar, escribir en binario. La versión para archivos pequeños es un trabajo de una función:
#include <cstdio>
#include <fstream>
#include <iterator>
#include <string>
#include <vector>
/* base64_encode de la sección "Cuarenta líneas, cero dependencias" */
std::string encode_file(const std::string &path) {
std::ifstream in(path, std::ios::binary);
if (!in) return {};
std::vector<unsigned char> bytes{std::istreambuf_iterator<char>(in),
std::istreambuf_iterator<char>()};
return base64_encode(
std::string(reinterpret_cast<const char *>(bytes.data()), bytes.size()));
}
int main() {
std::string b64 = encode_file("/etc/hostname");
std::printf("file -> %zu chars\n", b64.size());
}
El techo es el hecho específico de C++ del título de la sección: cada parámetro de longitud de la API EVP es un int. Una sola llamada a EVP_EncodeBlock puede por tanto codificar como máximo unos 2 GB de entrada, y el buffer de salida de esa llamada - 1,33 veces más grande - no cabe en un int en absoluto. Por debajo del techo, la API de bloques va bien para archivos que caben en memoria. Por encima, o para un archivo que no quieras en memoria, haces trozos - y la regla de troceado es la única restricción específica de base64 sobre el bucle: los trozos deben ser múltiplos de 3 bytes, porque el agrupamiento es de tres en tres y un límite de trozo en medio de un grupo cambia la salida. 3072 - tres trozos de 1024 bytes - es un tamaño de trozo cómodo, y el bucle se convierte en:
#include <algorithm>
#include <cstddef>
#include <string>
/* base64_encode de la sección "Cuarenta líneas, cero dependencias" */
std::string encode_streamed(const std::string &data) {
std::string out;
for (size_t pos = 0; pos < data.size();) {
size_t take = std::min<size_t>(3072, data.size() - pos);
out += base64_encode(data.substr(pos, take));
pos += take;
}
return out;
}
Cada trozo codifica de forma independiente y la concatenación es idéntica al resultado one-shot - que es la propiedad que hace seguro el troceado en absoluto, y sale directa del agrupamiento de 3 bytes. (El contexto por streaming de OpenSSL de la sección del codificador hace el mismo trabajo mientras añade la envoltura de líneas de 64 caracteres gratis, que es la herramienta correcta cuando el consumidor quiere forma MIME.) Y el lado de salida tiene el mismo presupuesto que el lado de entrada: un archivo de 10 GB se convierte en una cadena de 13,3 GB, así que el buffer - o el archivo que estás escribiendo - se dimensiona con la fórmula de la sección de matemáticas, y el techo de int dice que la ruta por trozos no es una comodidad por encima de 2 GB - es la única ruta.
Variables de entorno y línea de comandos
Las variables de entorno tienen el mismo problema que las cadenas JSON y una respuesta peor: no pueden llevar bytes NUL en absoluto, y los caracteres de control tampoco son sus amigos. El truco estándar es hacer base64 del payload para que sobreviva al shell, y en C++ la dirección de codificación es un one-liner:
#include <cstdio>
#include <cstdlib>
#include <string>
/* base64_encode de la sección "Cuarenta líneas, cero dependencias" */
int main() {
setenv("MY_PAYLOAD", base64_encode("hello, env").c_str(), 1);
std::printf("env: %s\n", getenv("MY_PAYLOAD"));
}
El valor que aterriza en el entorno es aGVsbG8sIGVudg==: alfabeto puro, seguro para el shell, seguro para un archivo .env, seguro para un dashboard de CI, y decodificable en cualquier máquina que tenga un decodificador base64. La línea de comandos en sí tiene la misma historia de dos herramientas que el lado de decodificación, con las banderas de la dirección de codificación: base64 de coreutils (o la reimplantación de uutils que traen las distribuciones más nuevas; comprueba con base64 --version) envuelve a 76 por defecto, y -w 0 te da una línea; openssl base64 - el programa enc comprobando su propio nombre en argv[0] y conmutando a modo base64 - envuelve a 64 y acepta -A para una sola línea:
# una línea, para tokens y configuración
base64 -w 0 < payload.bin > payload.b64
openssl base64 -A < payload.bin > payload.b64
# envuelto, para email y archivos de texto
base64 < payload.bin > payload-76.b64
openssl base64 < payload.bin > payload-64.b64
Ninguno habla base64url de forma nativa, así que un token que acuñes en un shell recibe el tratamiento de transcode antes de ir a una URL. Y la línea de comandos es donde el hábito de fallo silencioso del lado de codificación es más peligroso: un codificador que envolvió cuando tu consumidor esperaba una línea no dará error, simplemente producirá una cadena con saltos de línea dentro - que es exactamente el fallo que ahora persigues en producción. Para lo que importe, codifica en tu programa, donde el buffer está dimensionado por la fórmula y la forma de las líneas es una variable que tú controlas.
Trampas: edición C++
- El NUL que no pediste.
EVP_EncodeBlockañade un terminador NUL después del payload. El ejemplo de la man page: 16 bytes de entrada, 24 codificados más el NUL, 25 en el buffer, 24 devueltos. Dimensiona para el byte extra y recorta al valor devuelto, o tu token termina en un byte a cero. - El 64 duro. La API por streaming de OpenSSL envuelve a 64 caracteres, cada bloque termina en un salto de línea, y no hay ninguna bandera para cambiarlo. La salida de un codificador envuelta en un consumidor de una línea es un bug de carácter de control.
- El bloque de 48 bytes.
EVP_EncodeUpdatesolo emite salida para bloques de entrada completos de 48 bytes; el resto se queda en el contexto hastaEVP_EncodeFinal. Presupuesta 65 bytes de salida por bloque más el NUL, y no leas*outlcomo "bytes de mi payload" - son los bytes que esta llamada escribió, que para una primera llamada pequeña son cero. - Los pads faltantes del iterador. La cadena de Boost.Serialization nunca emite
=. Un iterador de 2002 codificando "Mane" te da seis caracteres. Añade los pads tú, o tu consumidor estricto rechazará la cadena. - El CRLF que no pediste.
CryptBinaryToStringAañade una pareja CR/LF a menos que pasesCRYPT_STRING_NOCRLF. Un token base64 construido con las banderas por defecto es dos caracteres más largo de lo que debería, y el penúltimo carácter es un retorno de carro. - La llamada NULL cuenta el NUL. La sonda de tamaño de Windows devuelve la longitud necesaria incluyendo la null terminadora; la llamada de verdad devuelve la longitud sin ella. Confundir las dos es el off-by-one clásico, y escribe un byte más allá del buffer o pierde el último carácter.
- Envolver después, no durante. La envoltura de líneas es un paso de post-procesado sobre la cadena codificada. Corta en múltiplos del ancho - siempre seguro, porque cada grupo de 4 caracteres es autocontenido - y nunca envuelvas los bytes crudos, que es donde no pertenecen los saltos de línea.
- Trozos de tres. Si codificas un payload grande en pedazos, los límites de pedazo deben caer en grupos de 3 bytes, o el agrupamiento - y la salida - cambia. 3072 es un trozo amable; 3071 es un bug.
- int, no size_t. Cada parámetro de longitud de EVP es un
int. El techo de una sola llamada son unos 2 GB de entrada, y la salida para esa entrada no cabe en uninten absoluto. Por encima del techo, la ruta por trozos o por streaming no es una preferencia. - char con signo. Si empaquetas desde un
char *sin el cast a unsigned, un byte por encima de 127 es un número negativo en plataformas dondechares con signo, e indexar una tabla con eso es comportamiento indefinido.const unsigned char *no es ceremonia. - La cadena JSON envuelta. Un valor envuelto a 76 caracteres pegado en un archivo JSON es una cadena de caracteres de control literales. O es una línea, o está escapado, o no está en JSON.
- Los pads son un contrato. Algunos consumidores quieren padding (MIME, la mayoría de decodificadores), otros no (JWT, PKCE, tokens en URLs), y unos pocos estrictos rechazan de plano los pads faltantes o no canónicos. El pad no es decoración; es parte del acuerdo del formato.
- Los dos alfabetos. Una
-o_dentro de un payload de alfabeto estándar es inválida, y una+o/dentro de uno URL-safe es inválida. Los alfabetos no son intercambiables a nivel de byte - codifica con el correcto para el destino, y transcoda deliberadamente. - std::string y strlen.
std::stringlleva bytes a cero contentísimo, pero en el momento en que le pasas una cadena de C a una API heredada,strlense detiene en el primer NUL. Pasa puntero y longitud, nunca un puntero a secas. - El presupuesto. La salida es 4/3 de la entrada: si la entrada es 1,5 GB, la salida es 2 GB - que es también el techo de
int. Dimensiona el buffer receptor, la columna y el cable con la fórmula, no con una suposición.
Cómo se hizo C++ con su Base64
La historia del formato es más antigua que la era moderna del lenguaje, y la historia de C++ es la historia de un lenguaje que repetidamente no la distribuye. 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ó 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 C de 2004-2008 de Rene Nyffenegger, que una pregunta de Stack Overflow del 4 de diciembre de 2008 repartió por la web. La parte más bonita de esa historia: una respuesta enlazó con la propia página de Nyffenegger y se llevó la implementación entera, cabecera de licencia incluida, y una respuesta con los votos más altos benchmarkó su solución contra el resto del campo. La canción de pueblo tiene cabecera de licencia - el compositor en persona nunca se presentó en los comentarios.
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 en la dirección de decodificación y famosamente sin padding en la dirección de codificación, un año antes de que el RFC 3548 codificara las reglas del alfabeto 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. El EVP_EncodeBlock de OpenSSL y sus compañeros han estado en cada versión de OpenSSL, así que el caballo de batalla lleva en la caja de herramientas desde que el lenguaje lleva discutiendo si debería estar en el estándar. En Windows, la historia es simplemente que el sistema operativo la distribuyó: una función, una tabla de banderas, ningún estándar involucrado. Mientras tanto el estándar en sí fue pasando por C++11, C++14, C++17, C++20, C++23 (publicado en 2024), y ahora C++26, y cada uno de ellos miró el alfabeto de 64 caracteres y siguió de largo. El contenido técnico de C++26 se terminó y fue votado a favor (114-12-3) en la reunión ISO de C++ de marzo de 2026 en Croydon, Reino Unido, y sí añade una nueva cabecera <text_encoding> para el trabajo de codecs de texto; las reuniones siguientes del comité, en junio de 2026 (Brno) y noviembre de 2026 (Búzios, Brasil), abren el borrador de trabajo de C++29 en vez de revisar C++26. Base64 no está en el estándar. Ocho 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: un par de EVP, dos sabores de Boost, una bandera de Windows, y un fragmento de cuarenta líneas que es tuyo.
Detalles que vale la pena conocer
- El bloque de 48 bytes del codificador por streaming de OpenSSL es un número que no aparece en ningún RFC. Son 16 grupos base64, elegidos para que la línea de salida sea exactamente de 64 caracteres - el hábito de PEM - y es uno de los últimos sitios donde 1987 sigue haciendo trabajo estructural en 2026.
- El
encoded_sizede Boost.Beast es la sección de matemáticas como funciónconstexpr:4 * ((n + 2) / 3), evaluada en tiempo de compilación cuando le das una constante. La biblioteca estándar nunca se llevó este one-liner; Boost lo distribuyó en un espacio de nombresdetail::en su lugar. - El base64 con padding más pequeño posible son cuatro caracteres,
QQ==: un byte con un disfraz de dos caracteres. El más pequeño sin padding son dos caracteres,QQ. El recuento de pads es también un mensaje: dos pads significa que el último grupo tenía un byte, un pad significa que tenía dos, y sin pads significa que tenía tres - el receptor puede recuperar la longitud de la entrada únicamente del final. - La matemática de sobrecarga 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 814 bytes de cabeceras. Cada codificador de este artículo paga el mismo impuesto; el ancho de envoltura solo cambia cómo se factura.
- En un libstdc++ o MSVC típico,
std::stringlleva payloads pequeños en un buffer de pila mediante la small-string optimization en vez de reservar. Una entrada de 9 bytes codifica a 12 caracteres y nunca toca la heap. La forma base64 de tu token 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 base64al que puedes acudir en un shell no es un comando en absoluto. Es el programaenccomprobando su propio nombre enargv[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. - Cuatro A -
AAAA- codifican tres bytes a cero, porque A es el cero del alfabeto. Si alguna vez has visto un blob base64 hecho enteramente de un carácter, ya sabes lo que estaba diciendo: nada. - 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".
La otra dirección
Todo lo que acabas de empaquetar será desempacado en el otro lado por la misma caja de herramientas, y el lado de desempaque tiene su propio conjunto de hábitos: la función one-shot de OpenSSL que rellena el final con ceros, el bugfix de 2025 que cambió lo que devuelve el decodificador por streaming para entrada con padding, el decode de Boost.Beast que se detiene en un carácter suelto y no dice una palabra, el iterador que lanza ante un solo espacio, y el decodificador estricto de cuarenta líneas que señala el byte exacto que dolió. La historia completa del desempaque - los temperamentos de los cuatro decodificadores, el transcode de base64url, archivos, el hábito de 76 caracteres de MIME, y las dos herramientas de línea de comandos que fallan en silencio - vive en la guía de decodificación C++ del sitio hermanado. Ve a leerla, y luego vuelve y empaqueta algo grande. Ese es todo el juego: sin biblioteca estándar, cuatro proveedores con cuatro opiniones distintas sobre saltos de línea y NULs, una fórmula que dimensiona cada buffer del artículo, y un impuesto del 33 por ciento que cada receptor tiene derecho a reclamar. Feliz empaquetado.
Última actualización: 2026-09-08
Artículo relacionado: Decodificación Base64 en C++ (Cpp): una guía completa