Decodificación Base64 en C: una guía completa
Está en una respuesta de API, un archivo de configuración, un adjunto de correo o en mitad de una URL: una larga cadena de letras, dígitos, el ocasional + o /, y quizás un = o dos al final. Lo reconoces al instante, y ahora necesitas los bytes originales de vuelta - en C. Ese es todo el trabajo de la decodificación Base64: entran cuatro caracteres del alfabeto, salen tres bytes crudos, una y otra vez, hasta que las marcas = te dicen dónde terminaron los datos reales. La página de inicio de este sitio repasa el formato paso a paso, así que este artículo gasta su energía donde está el trabajo de verdad: en buffers, bibliotecas y las trampas que viven entre medias.
Dos cosas por saber antes del primer malloc. Primero, decodificar es la dirección que encoge: la salida es tres cuartas partes del tamaño de la entrada, así que un decodificador nunca necesita más memoria que el payload que ya tiene en la mano. Segundo - y este es el titular - C no viene con un decodificador Base64. La biblioteca estándar del idioma se congeló mucho antes de que existiera Base64, y ningún estándar posterior ha llenado el hueco. Así que todos los programas en C que decodifican Base64 se apoyan en una biblioteca, y las cuatro que importan en la práctica son OpenSSL, Mbed TLS, APR-Util y GLib. Cada una tiene una personalidad distinta: lo que perdona, cómo reporta los errores y lo que hace en silencio con tu salida. Cuando conozcas la personalidad de tu decodificador, decodificar Base64 en C deja de ser una fuente de bugs misteriosos y se convierte en una rutina que puedes escribir dormido.
La caja de herramientas: cuatro formas de recuperar tus bytes
Este es el panorama de un vistazo. Las cuatro cubren el alfabeto estándar; las diferencias están en los bordes, y los bordes son de donde salen los bugs.
| Biblioteca | Cabecera | Modelo de errores | Particularidad de salida que recordar |
|---|---|---|---|
| OpenSSL (libcrypto) | <openssl/evp.h> |
Devuelve -1 con entrada mala |
El decodificador one-shot rellena el final con ceros |
| Mbed TLS | <mbedtls/base64.h> |
Códigos de retorno (-0x002C, -0x002A) |
Las reglas de entrada más estrictas de las cuatro |
| APR-Util | <apr-1.0/apr_base64.h> |
Ninguno: se detiene en el primer carácter raro | API con longitudes int, así que 2 GB es el techo |
| GLib | <glib.h> |
Devuelve NULL solo con fallo total |
Ignora en silencio la basura suelta |
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 Mbed TLS: libmbedtls-dev (o mbedtls). Para APR-Util: libaprutil1-dev más libapr1-dev. Para GLib: glib2.0-dev. Después enlazas con -lcrypto, -lmbedcrypto, -laprutil-1 o -lglib-2.0 respectivamente. ¿Cuál eliges? Si ya enlazas OpenSSL para TLS o hashing (la mayoría de los servidores lo hacen), usa OpenSSL. Para builds embebidos y con recursos limitados, Mbed TLS es el ciudadano pequeño y estricto. Si estás dentro del ecosistema Apache, APR-Util ya está ahí. Si tu base de código es GNOME o GTK, GLib mantiene todo en un solo runtime.
OpenSSL: el decodificador que rellena los huecos con ceros
OpenSSL trae Base64 en dos sabores. La función one-shot es la protagonista de la mayoría del código:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
unsigned char out[16];
const char *payload = "TWFuZQ==";
int n = EVP_DecodeBlock(out, (const unsigned char *)payload,
(int)strlen(payload));
if (n < 0) {
printf("not base64\n");
return 1;
}
printf("%d bytes\n", n);
return 0;
}
Le pasas un buffer de caracteres Base64 y una longitud, y escribe los bytes decodificados en out y devuelve cuántos. Recorta el espacio en blanco inicial, recorta el espacio en blanco y los saltos de línea finales, y rechaza la entrada que no es un múltiplo de cuatro caracteres tras recortar, o que contiene un carácter fuera del alfabeto. Hasta aquí, un contrato perfectamente sensato. Excepto por un detalle que ha corrompido más de una importación de base de datos en silencio: el valor de retorno no es la longitud real de los datos.
Ejecuta ese programa y obtendrás 4 bytes... no, espera. TWFuZQ== son dos grupos de cuatro caracteres, así que la función devuelve 6, y el buffer contiene 4d 61 6e 65 00 00: la palabra "Mane" más dos bytes de cero. El decodificador one-shot de OpenSSL trabaja en cuantías fijas - cada cuatro caracteres de entrada producen siempre exactamente tres bytes de salida - y cuando el último grupo solo llevaba un byte real, las otras dos posiciones se rellenan con ceros. El manual lo menciona en una sola frase calmada ("la salida se rellenará con bits 0 si es necesario"), y esa frase es la más importante de toda la página de man de esta función.
La longitud real se recupera desde el padding, y es un cálculo de dos líneas:
size_t real_length(const char *b64) {
size_t len = strlen(b64);
while (len > 0 && b64[len - 1] == '=') len--;
return len * 3 / 4;
}
Cuenta los caracteres del alfabeto, descarta los pads finales, multiplica por tres, divide por cuatro. Para TQ== (la letra M, codificada), eso da (2 * 3) / 4 = 1 byte real - mientras EVP_DecodeBlock reportará tres. Mantén siempre el par (puntero, longitud) junto, y no uses nunca strlen sobre datos decodificados, porque los bytes que te devolvieron pueden ser un JPEG y el primero de ellos puede ser un NUL.
El decodificador por streaming: un decodificador que sabe cuándo parar
Para todo lo demás, OpenSSL ofrece el par de streaming EVP_DecodeUpdate más EVP_DecodeFinal. El objeto de contexto es lo que mueve el estado entre llamadas: guarda de uno a tres caracteres de un grupo inacabado para que puedas dar el payload en trozos. El comportamiento que importa es este: el espacio en blanco (espacios, tabs, retorno de carro, salto de línea) se salta en cualquier parte del stream, cualquier otro carácter no del alfabeto o un = a mitad de los datos devuelve -1 inmediatamente, y un retorno de 0 de la update significa "se ha visto el padding, no se espera nada más". EVP_DecodeFinal entonces se niega con -1 si aún hay un grupo parcial pendiente, porque una longitud que no es múltiplo de cuatro (después del espacio en blanco) no es un payload válido.
Una nota de versión antes del código, porque los tutoriales viejos te harán tropezar: en OpenSSL 3.x el tipo de contexto EVP_ENCODE_CTX es opaco, así que el patrón de pila EVP_ENCODE_CTX ctx; que hay en mucho código de internet ya no compila. Asigna y libera explícitamente:
static int decode_b64(const unsigned char *in, int in_len,
unsigned char *out, int *out_len) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
if (ctx == NULL) {
return -1;
}
*out_len = 0;
EVP_DecodeInit(ctx);
int r = EVP_DecodeUpdate(ctx, out, out_len, in, in_len);
if (r < 0) {
EVP_ENCODE_CTX_free(ctx);
return -1;
}
int tail = 0;
r = EVP_DecodeFinal(ctx, out + *out_len, &tail);
EVP_ENCODE_CTX_free(ctx);
if (r < 0) {
return -1;
}
*out_len += tail;
return 0;
}
Dimensiona el buffer de salida en in_len * 3 / 4 + 3 y la llamada es segura para cualquier entrada. Mira cómo maneja un payload envuelto en MIME donde el salto de línea cae a mitad de un grupo:
const char *wrapped = "TWFu\nZQ==";
unsigned char out[16];
int out_len = 0;
if (decode_b64((const unsigned char *)wrapped,
(int)strlen(wrapped), out, &out_len) != 0) {
printf("invalid base64\n");
return 1;
}
printf("%.*s\n", out_len, out); /* Mane */
El salto desaparece, los cuatro bytes salen, y nadie tuvo que pre-limpiar la entrada. Hay una diferencia de regalo frente a la función one-shot: la vía de streaming cuenta los bytes con honestidad. Dale TQ== y devuelve exactamente un byte (4d), sin padding de ceros, porque entiende que dos pads significan que dos de las tres posiciones de salida nunca se llenaron. Si tu payload necesita alguna vez una longitud de confianza desde OpenSSL, esta es la vía que hay que usar.
Mbed TLS: la estricta
Mbed TLS (la biblioteca cripto que empezó su vida como PolarSSL y ahora se envía dentro de los stacks embebidos de ARM) te da dos funciones con un contrato muy limpio:
int mbedtls_base64_encode(unsigned char *dst, size_t dlen, size_t *olen,
const unsigned char *src, size_t slen);
int mbedtls_base64_decode(unsigned char *dst, size_t dlen, size_t *olen,
const unsigned char *src, size_t slen);
Decodifica como lo haría una persona cuidadosa. Llámala con dst puesto en NULL (o dlen a cero) y te dice el tamaño necesario en *olen sin hacer ningún trabajo; llámala de verdad y obtienes 0 en éxito, MBEDTLS_ERR_BASE64_INVALID_CHARACTER (eso es -0x002C) si algo de la entrada está mal, o MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL (eso es -0x002A) si el destino es demasiado pequeño. La longitud decodificada aterriza en *olen, y a diferencia de la función one-shot de OpenSSL siempre es el número honesto: decodificar TQ== te da un byte, 4d, nada más.
Las reglas de entrada son las más estrictas de las cuatro bibliotecas, y merecen memorizarse porque definen lo que significa "válido" para Mbed TLS:
- Los saltos de línea CRLF y LF pueden aparecer entre grupos - los payloads de correo funcionan tal cual.
- Los espacios están permitidos justo antes de un salto de línea y al final del buffer, pero un espacio después de un salto de línea o a mitad de un grupo es un error.
- Como máximo dos caracteres
=, y solo al final; cualquier dato después de un pad es un error. - Cualquier byte por encima de 127 (acentos, fragmentos UTF-8, basura binaria) es un error.
Esa última regla es la que muerde: si un payload llega de una fuente que destrozó la codificación de caracteres, Mbed TLS lo rechazará donde un decodificador más perezoso lo habría decodificado con encogida de hombros. Para cualquier cosa que toque entrada no confiable, estricto es una característica. Una decodificación completa se ve así:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/base64.h>
int main(void) {
const char *payload = "TWFuZQ==";
size_t need = 0;
int rc = mbedtls_base64_decode(NULL, 0, &need,
(const unsigned char *)payload,
strlen(payload));
if (rc != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) {
printf("size query failed: %d\n", rc);
return 1;
}
unsigned char *out = malloc(need);
size_t olen = 0;
rc = mbedtls_base64_decode(out, need, &olen,
(const unsigned char *)payload,
strlen(payload));
if (rc != 0) {
printf("decode failed: %d\n", rc);
free(out);
return 1;
}
printf("%.*s\n", (int)olen, out);
free(out);
return 0;
}
(Que la consulta de tamaño devuelva el código "demasiado pequeño" es por diseño: es cómo la función reporta lo que habría escrito. Los dos códigos de retorno de arriba vienen de <mbedtls/base64.h>, la misma cabecera donde la función está declarada.)
APR-Util y GLib: dos sillas más
APR-Util - la biblioteca de utilidades del Apache Portable Runtime, el cimiento sobre el que se construye Apache HTTP Server - lleva Base64 desde que el servidor necesita decodificar cabeceras de autenticación Basic. La API es una pequeña familia de funciones basadas en int:
#include <apr-1.0/apr_base64.h>
int apr_base64_encode_len(int len);
int apr_base64_encode(char *coded_dst, const char *plain_src,
int len_plain_src);
int apr_base64_decode_len(const char *coded_src);
int apr_base64_decode(char *plain_dst, const char *coded_src);
Dos cosas por saber antes de echar mano de ella. Primero, las longitudes son int: de 32 bits, así que el techo práctico es 2 GB por llamada, que va bien para cabeceras y valores de configuración y no va bien para decodificar un archivo de 4 GB. Segundo - y esta es la grande - la función de decodificación no tiene retorno de error alguno. El comportamiento solo se ve en la implementación, no en la cabecera: el decodificador toma cualquier carácter inválido, incluido el espacio en blanco y el NUL, como un terminal. Decodifica hasta lo primero que no reconoce, devuelve hasta dónde llegó, y no dice nada. Un payload truncado, un texto pegado con un comentario al final, un byte corrupto a mitad - todo eso produce una salida más corta en silencio. Si usas el decodificador de APR, debes comparar la longitud devuelta con lo que el payload prometió; la función no lo hará por ti. No hay un envoltorio que asigne desde el pool - tú proporcionas el buffer de destino, así que en código dirigido por pools asignas plain_dst del pool tú mismo. También hay un ángulo EBCDIC que no verás en ningún otro lugar de este artículo: en máquinas EBCDIC las funciones convierten la entrada a ASCII antes de codificar y de vuelta después de decodificar, así que el mismo código corre en los mainframes que todavía hacen funcionar httpd.
GLib, el runtime detrás de GTK y la mayoría de las aplicaciones GNOME, toma la personalidad opuesta. Su decodificador acepta una cadena y siempre devuelve un buffer recién asignado (NULL solo si le pasas un puntero NULL), decodificando lo que puede y saltando en silencio el resto:
#include <glib.h>
gsize out_len = 0;
guchar *bytes = g_base64_decode(payload, &out_len);
if (bytes == NULL) {
printf("not base64\n");
} else {
printf("%u bytes\n", (unsigned)out_len);
g_free(bytes);
}
La trampa está en la palabra "siempre". El decodificador de GLib está en la escuela tolerante: los caracteres fuera del alfabeto se saltan, no son fatales. Dale TWFuZ@== y te devolverá los tres bytes de "Man" sin mover un dedo. También hay una variante in-place cómoda, g_base64_decode_inplace(), que decodifica por encima del buffer de entrada (segura porque la salida es más corta que la entrada) y devuelve el mismo puntero, así que el resultado empieza al principio del buffer - un buen truco para código con poca memoria, y come con gusto la entrada envuelta en CRLF. La moraleja para desarrolladores de C: si tus datos no son confiables, GLib no te salvará de un payload corrupto. Las variantes _step (g_base64_decode_step con un entero de estado) están disponibles cuando necesitas decodificación incremental, y el par correspondiente g_base64_encode_step/g_base64_encode_close vive en el lado de la codificación.
Base64 URL-safe: el otro alfabeto
En algún punto entre el alfabeto estándar y tus URLs, alguien se hizo daño. El Base64 estándar usa + y / como sus dos símbolos más altos, y ambos son problema en URLs: un + en una query string se interpreta rutinariamente como espacio para cuando tu servidor lo ve, y / es un separador de ruta. La RFC 4648, sección 5, define la solución, llamada base64url: la misma codificación con + sustituido por -, / sustituido por _, y el padding final de = descartado cuando la longitud se conoce por otro medio. Los JSON Web Tokens, los parámetros de estado OAuth y un montón de IDs de sesión de API viven en este dialecto.
Ninguna de las cuatro bibliotecas de C decodifica base64url de forma nativa, así que la conversión es un pequeño helper que escribes una vez y reutilizas: mapea los dos caracteres especiales de vuelta, vuelve a añadir el padding que falte, y luego pasa el resultado a tu decodificador estándar. La comprobación de longitud va primero, porque una longitud de uno más que un múltiplo de cuatro es imposible en cualquier dialecto de Base64:
int base64url_decode(const char *url_safe, unsigned char *out,
size_t out_cap, size_t *out_len) {
size_t len = strlen(url_safe);
if (len % 4 == 1) {
return -1;
}
size_t needed = (len * 3) / 4;
if (needed > out_cap) {
return -2;
}
char *std = malloc(len + 4);
if (std == NULL) {
return -3;
}
for (size_t i = 0; i < len; i++) {
char c = url_safe[i];
if (c == '-') c = '+';
if (c == '_') c = '/';
std[i] = c;
}
size_t pad = (4 - len % 4) % 4;
for (size_t i = 0; i < pad; i++) {
std[len + i] = '=';
}
int n = EVP_DecodeBlock(out, (const unsigned char *)std,
(int)(len + pad));
free(std);
if (n < 0) {
return -1;
}
*out_len = needed;
return 0;
}
Dos trampas custodian este camino. La primera es la dirección: si le pasas un payload URL-safe al decodificador estándar sin el cambio de caracteres, OpenSSL y Mbed TLS lo rechazan (esos caracteres no están en su alfabeto), mientras GLib saltará en silencio el - y el _ y devolverá una cadena más corta de lo que debería - sin ningún error. Pasa siempre por el helper. La segunda es la propia advertencia de la RFC, que vale la pena tomar en serio: base64url "no debe considerarse lo mismo que la codificación base64". Si un payload por casualidad no contiene caracteres - o _, los dos dialectos son idénticos byte a byte para esos datos, y un mix-up es invisible - que es exactamente por lo que el mix-up sobrevive hasta que choca con un payload que sí contiene uno.
Archivos: restaurando el original
El trabajo con forma de archivo más común es lo contrario de lo que hizo alguna rutina de exportación: llega un archivo de texto .b64, y necesitas el archivo original de vuelta. Lee todo el texto, decodifícalo, y deja que los bytes se presenten antes de confiar en cualquier etiqueta. C no tiene finfo, así que la prueba práctica es un olfateo de números mágicos sobre los primeros bytes:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int main(void) {
FILE *f = fopen("upload.b64", "rb");
if (f == NULL) {
return 1;
}
fseek(f, 0, SEEK_END);
long size = ftell(f);
fseek(f, 0, SEEK_SET);
char *text = malloc((size_t)size + 1);
size_t got = fread(text, 1, (size_t)size, f);
fclose(f);
text[got] = '\0';
unsigned char *out = malloc((got * 3) / 4 + 3);
int out_len = 0;
if (decode_b64((const unsigned char *)text, (int)got,
out, &out_len) != 0) {
printf("not valid base64\n");
free(text);
free(out);
return 1;
}
free(text);
const char *kind = "unknown binary";
if (out_len >= 4 && memcmp(out, "\x89PNG", 4) == 0) kind = "png";
else if (out_len >= 5 && memcmp(out, "%PDF-", 5) == 0) kind = "pdf";
else if (out_len >= 4 && memcmp(out, "PK\x03\x04", 4) == 0) kind = "zip";
else if (out_len >= 3 && memcmp(out, "\xff\xd8\xff", 3) == 0) kind = "jpeg";
printf("looks like a %s, %d real bytes\n", kind, out_len);
free(out);
return 0;
}
Notas sobre los bordes: abre el archivo en modo binario (rb/wb) incluso para la mitad de texto, porque el modo texto traducirá los finales de línea en algunas plataformas y corromperá tu conteo de caracteres; y no hagas printf("%s") del buffer decodificado para "ver qué es". El olfateo mágico es la forma honesta de hacer esa pregunta, y si más adelante sirves el archivo restaurado a un navegador, el Content-Type debería salir del mismo olfateo, no del nombre de archivo.
Data URIs: la imagen dentro de la URL
Una llegada favorita del mundo web: alguien pega una imagen en un formulario, y el front end le pasa a tu servidor una data URI completa como data:image/png;base64,iVBORw0KGgo.... La RFC 2397 define la forma: data:, un tipo de medio opcional, un flag ;base64 opcional, una coma, y luego el payload. Cuando el flag está presente, el payload es Base64; cuando no, el payload es texto plano percent-encoded - más raro, pero legal. Si el tipo de medio se omite, el valor por defecto es text/plain;charset=US-ASCII. Analizarlo en C es cuestión de encontrar la coma y mirar qué hay justo antes:
int split_data_uri(const char *uri, char *mime, size_t mime_cap,
int *is_b64, const char **payload) {
if (strncmp(uri, "data:", 5) != 0) {
return -1;
}
const char *comma = strchr(uri, ',');
if (comma == NULL) {
return -1;
}
*is_b64 = 0;
const char *meta = uri + 5;
size_t meta_len = (size_t)(comma - meta);
if (meta_len >= 7 && strncmp(comma - 7, ";base64", 7) == 0) {
*is_b64 = 1;
meta_len -= 7;
}
if (meta_len == 0) {
snprintf(mime, mime_cap, "text/plain;charset=US-ASCII");
} else {
snprintf(mime, mime_cap, "%.*s", (int)meta_len, meta);
}
*payload = comma + 1;
return 0;
}
Y el código que llama se lee como una frase:
char mime[256];
int is_b64 = 0;
const char *payload = NULL;
const char *uri = "data:image/png;base64,iVBORw0KGgo...";
if (split_data_uri(uri, mime, sizeof(mime), &is_b64, &payload) == 0) {
printf("mime=%s base64=%d\n", mime, is_b64);
/* ahora decodifica el payload con la biblioteca que elijas */
}
Tres trampas viven en este formato. El flag ;base64 ausente es la primera: una data URI legal sin él lleva un payload percent-encoded, y pasar eso por un decodificador Base64 produce basura - comprueba el flag y luego elige tu decodificador. El tipo de medio declarado es la segunda: es una pista del emisor, no un hecho; el olfateo de números mágicos de la sección de archivos es tu hecho. La tercera es el tamaño: el propio consejo de la RFC es que las data URIs son para valores cortos, así que una imagen de varios megabytes montada dentro de una URL es una mala señal en tu arquitectura, no un patrón que celebrar.
JWTs: leyendo las partes no secretas
El payload Base64 más famoso de la web es el JSON Web Token, y el menos aterrador una vez que conoces su forma. Según la RFC 7519, un JWT compacto es tres partes base64url unidas por puntos: una cabecera, un payload y una firma - cada una codificada sin padding, sin saltos de línea. Las dos primeras partes son JSON plano, por eso todos pueden leerlas, y por eso todos deberían seguir leyendo antes de tocar un token.
Leer las dos primeras partes lleva unas líneas con el helper base64url de arriba, y es la forma más rápida de desmitificar un token:
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int base64url_decode(const char *url_safe, unsigned char *out,
size_t out_cap, size_t *out_len);
int main(void) {
const char *token =
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
"eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0."
"TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";
const char *dot1 = strchr(token, '.');
if (dot1 == NULL) {
return 1;
}
const char *part2 = dot1 + 1;
const char *dot2 = strchr(part2, '.');
if (dot2 == NULL) {
return 1;
}
const char *part3 = dot2 + 1;
char seg[512];
char buf[1024];
size_t n = 0;
size_t hlen = (size_t)(dot1 - token);
memcpy(seg, token, hlen);
seg[hlen] = '\0';
if (base64url_decode(seg, (unsigned char *)buf,
sizeof(buf), &n) == 0) {
printf("header: %.*s\n", (int)n, buf);
}
size_t plen = (size_t)(dot2 - part2);
memcpy(seg, part2, plen);
seg[plen] = '\0';
if (base64url_decode(seg, (unsigned char *)buf,
sizeof(buf), &n) == 0) {
printf("payload: %.*s\n", (int)n, buf);
}
printf("signature: %s (encoded, verify before trusting!)\n", part3);
return 0;
}
Impreso, la cabecera es {"alg":"HS256","typ":"JWT"} y el payload es {"sub":"1234567890","name":"John Doe"}. Ahora la parte que importa: la tercera parte es una firma, y las dos partes que acabas de decodificar no son secretas ni autenticadas. Cualquiera con una captura de paquetes puede leerlas, y cualquiera con un editor de texto puede reescribirlas. Confiar en un payload JWT en C antes de verificar la firma es el bug de autenticación clásico, y Base64 hace que sea fácil no notarlo - el token parece un blob inquebrantable siendo una postal. Para verificar un token HS256 recalculas el HMAC-SHA256 sobre header.part con tu secreto usando HMAC() de <openssl/hmac.h> y comparas en tiempo constante con CRYPTO_memcmp(); si los resúmenes no coinciden, el token se rechaza, sea lo que sea lo que alega. No hay una biblioteca JWT estándar de facto en C, así que para producción o construyes tú ese pequeño paso de verificación o adoptas una de las bibliotecas de la comunidad - pero el lado Base64 del trabajo es el baile de dividir-y-decodificar de arriba, y deberías entenderlo todo.
Basic Auth: la cabecera que nunca aprendió privacidad
La cabecera de autenticación más antigua de la web todavía viaja sobre Base64: Authorization: Basic seguido de la codificación en alfabeto estándar de username:password (la RFC 7617, que la RFC 9110 referencia para el esquema Basic). La RFC es explícita en que esto es codificación, no protección - cualquiera con una captura de paquetes puede decodificar ambas mitades con un solo comando - así que el trabajo del lado de la decodificación en C es analizar la cabecera, decodificar con estrictez, dividir en el primer dos puntos (las contraseñas pueden contener dos puntos legalmente) y comparar con una función segura frente a timing:
#include <string.h>
#include <openssl/evp.h>
#include <openssl/crypto.h>
static size_t real_length(const char *b64);
int basic_auth_ok(const char *header, const char *expected_user,
const char *expected_pass) {
if (strncmp(header, "Basic ", 6) != 0) {
return 0;
}
const char *b64 = header + 6;
unsigned char out[256];
int n = EVP_DecodeBlock(out, (const unsigned char *)b64,
(int)strlen(b64));
if (n < 0) {
return 0;
}
size_t real = real_length(b64);
size_t u_len = strlen(expected_user);
size_t p_len = strlen(expected_pass);
if (real != u_len + 1 + p_len) {
return 0;
}
if (memcmp(out, expected_user, u_len) != 0) {
return 0;
}
if (out[u_len] != ':') {
return 0;
}
return CRYPTO_memcmp(out + u_len + 1, expected_pass, p_len) == 0;
}
La comprobación de longitud hace trabajo de verdad: impide que un payload que decodifica a "alice:secret" con basura al final, o "alice:secre" truncado, llegue a coincidir. Y CRYPTO_memcmp (o memcmp solo si entiendes las implicaciones de timing) es lo que impide a un atacante ir midiendo su camino por tu lista de usuarios. Sirve esta cabecera por HTTPS o no la sirvas - en una conexión en claro la capa Base64 es solo adorno.
Correo y PEM: el hogar original
Base64 nació para un problema muy concreto: el transporte de correo solo llevaba ASCII de 7 bits, y la gente quería enviar binarios a través de él. MIME (la RFC 2045) hizo de Base64 una de las codificaciones de transferencia estándar y añadió dos normas de la casa: las líneas codificadas no deben superar los 76 caracteres, y el software de decodificación debe ignorar los caracteres fuera del alfabeto - saltos de línea incluidos. Esa segunda regla es la razón por la que los decodificadores por streaming de arriba mastican un adjunto envuelto con cero preprocesado, y es la razón por la que la costumbre de 76 caracteres sigue horneada en cada biblioteca de correo del planeta. El ancestro era PEM (Privacy Enhanced Mail, la RFC 1421), que usaba líneas de 64 caracteres en su lugar - la división 64/76 que ves en las herramientas es esa historia, ambos límites impuestos al final por SMTP.
La armadura PEM - el formato en el que viajan claves y certificados - es simplemente Base64 con etiqueta: una línea -----BEGIN ... -----, el cuerpo en líneas de 64 caracteres, y una línea END que hace juego. Quitar la armadura en C es un barrido por líneas, y luego el decodificador hace el resto:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
FILE *f = fopen("server.key", "r");
if (f == NULL) {
return 1;
}
char line[256];
char b64[8192];
size_t pos = 0;
int in_body = 0;
while (fgets(line, sizeof(line), f) != NULL) {
if (strncmp(line, "-----BEGIN", 10) == 0) {
in_body = 1;
continue;
}
if (strncmp(line, "-----END", 8) == 0) {
in_body = 0;
break;
}
if (in_body) {
size_t l = strlen(line);
while (l > 0 && (line[l - 1] == '\n' || line[l - 1] == '\r')) {
l--;
}
memcpy(b64 + pos, line, l);
pos += l;
}
}
fclose(f);
unsigned char der[8192];
int out_len = 0;
if (decode_b64((const unsigned char *)b64, (int)pos,
der, &out_len) != 0) {
printf("armor contained no valid base64\n");
return 1;
}
printf("DER payload decoded\n");
return 0;
}
Los bytes decodificados son DER, una serialización binaria compacta, y eso es lo que las funciones de certificados y claves de OpenSSL consumen al final. Dos notas: recoge el cuerpo sin sus saltos de línea (como hace el bucle) para que tu longitud sea múltiplo de cuatro, y si un archivo lleva varios bloques, empareja la etiqueta END con la etiqueta BEGIN que abriste - un flag simple funciona cuando solo quieres el primer bloque, como aquí.
Secretos, configuraciones y columnas de base de datos
Base64 es un contenedor de texto, y por eso sigue apareciendo en lugares donde no esperarías. En archivos de configuración y variables de entorno es el truco para colar valores que en otro caso romperían el formato: un DSN de base de datos con punto y comas, una contraseña con comillas, un valor con un salto de línea. En bases de datos, un blob binario puede vivir en una columna de texto como Base64 y sobrevivir a cada herramienta que asume texto - con un coste, eso sí, de alrededor de un tercio más de tamaño, así que dimensiona tus columnas en consecuencia (o pregúntate por qué el valor no está en una columna BLOB en primer lugar).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
static size_t real_length(const char *b64) {
size_t len = strlen(b64);
while (len > 0 && b64[len - 1] == '=') len--;
return len * 3 / 4;
}
int main(void) {
const char *b64 = getenv("API_KEY_B64");
if (b64 == NULL) {
printf("API_KEY_B64 is not set\n");
return 1;
}
size_t cap = strlen(b64);
unsigned char *out = malloc(cap);
int n = EVP_DecodeBlock(out, (const unsigned char *)b64, (int)cap);
if (n < 0) {
printf("API_KEY_B64 is not valid base64\n");
free(out);
return 1;
}
size_t real = real_length(b64);
printf("key is %zu bytes\n", real);
free(out);
return 0;
}
La cautela se aplica dos veces. Primero, esto es seguridad de formato, no secreto: en el momento en que un desarrollador puede leer el archivo de configuración, puede decodificar el valor con una sola llamada, y la sección de seguridad de la RFC registra incidentes reales donde la gente reportó un intercambio de protocolo al soporte y "reveló accidentalmente la contraseña" porque Base64 disfraza visualmente, no protege computacionalmente. Nunca guardes un secreto como Base64 y lo llames cifrado. Segundo, valida al arrancar: un valor de entorno a medio pegar es un -1 de la llamada estricta, y una comprobación de una línea convierte un fallo críptico tres horas después en un mensaje accionable en el arranque.
Decodificando desde la shell
No toda la decodificación pasa dentro de tu programa. Scripts CLI, trabajos cron y comandos de una línea decodifican Base64 constantemente, y los desarrolladores de C deberían conocer las dos herramientas que ya existen en cada máquina Linux. La herramienta de coreutils es la general: base64 -d decodifica, -i la hace ignorar caracteres basura en vez de fallar, y -w fija la columna de envoltura (que solo afecta a la codificación, no a la decodificación):
base64 -d < blob.b64 > blob.bin
base64 -d -i < messy.b64 > blob.bin
OpenSSL trae la suya, accesible como openssl base64 (un alias más amable de openssl enc -base64):
openssl base64 -d < blob.b64 > blob.bin
openssl base64 -d -A < blob.b64 > blob.bin
El flag -A significa "una línea": codifica sin el envoltado de 64 caracteres, y espera que la entrada sea también una sola línea. Y aquí hay una trampa de CLI que te costará una tarde si no la lees: la decodificación base64 de OpenSSL es orientada a líneas, y un payload que llega sin ningún salto de línea se decodifica a nada, en silencio:
printf 'TQ==' | openssl base64 -d | wc -c # 0
printf 'TQ==\n' | openssl base64 -d | wc -c # 1
El decodificador de coreutils no tiene esa expectativa, que es una de las razones por las que es el valor por defecto más seguro para el trabajo de pegamento. Una nota más de dialecto: los sistemas derivados de BSD (el macOS más viejo en particular) históricamente escribían el flag de decodificación como -D; las versiones modernas siguen la convención GNU de -d, así que mira la página de man de la máquina en la que estás realmente.
Payloads grandes, poca memoria
Decodificar es la dirección que te ayuda: la salida es tres cuartas partes del tamaño de la entrada, así que la presión de memoria por Base64 es rara. Aun así, cuando un archivo .b64 de cientos de megabytes aterriza en disco, la vía de streaming de antes es tu herramienta, y es más simple de lo que parece. Lee el archivo codificado en trozos, pasa cada trozo a EVP_DecodeUpdate, y escribe los bytes decodificados conforme llegan. El contexto guarda el uno a tres caracteres de cualquier grupo inacabado entre llamadas, así que los límites de trozo pueden caer donde sea - no necesitas alinearlos:
#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
EVP_DecodeInit(ctx);
FILE *in = fopen("huge.b64", "rb");
FILE *outf = fopen("huge.bin", "wb");
char inbuf[65536];
unsigned char outbuf[49152 + 4];
size_t got;
int ok = 1;
while (ok && (got = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
int outl = 0;
int r = EVP_DecodeUpdate(ctx, outbuf, &outl,
(const unsigned char *)inbuf, (int)got);
if (r < 0) {
ok = 0;
} else if (outl > 0) {
fwrite(outbuf, 1, (size_t)outl, outf);
}
}
int tail = 0;
if (ok && EVP_DecodeFinal(ctx, outbuf, &tail) == 1 && tail > 0) {
fwrite(outbuf, 1, (size_t)tail, outf);
}
EVP_ENCODE_CTX_free(ctx);
fclose(in);
fclose(outf);
return ok ? 0 : 1;
}
La memoria pico son dos buffers del orden de unas decenas de kilobytes sin importar el tamaño del archivo, y un archivo corrupto falla rápido - EVP_DecodeUpdate devuelve -1 en el trozo donde está el daño, así que puedes reportar un offset en vez de una encogida de hombros. Una salvedad de biblioteca para esta vía: el decodificador de APR-Util trabaja con cadenas terminadas en NUL y un registro de tamaño int (su valor de retorno es un int y la entrada se limita a justo menos de 3 GB por una constante interna), así que queda fuera de la carrera para archivos de varios gigabytes. Si necesitas reportar progreso, cuenta los bytes que has escrito - esa es tu posición en la salida, y la posición de entrada es aproximadamente cuatro tercios de ella.
Las trampas, todas específicas de C
Reunidas en un solo sitio, las trampas que son específicas de hacer esto en C:
- El one-shot relleno de ceros.
EVP_DecodeBlockdevuelve la longitud de la cuantía, no la longitud de los datos.TQ==reporta tres bytes pero lleva uno. Recalcula siempre la longitud real desde los pads finales, o usa el par de streaming. - Los bytes decodificados no son una cadena. El resultado puede contener bytes NUL y puede no ser UTF-8. Ni
strlen, niprintf("%s"), ni pasarlo a funciones que asuman texto. Lleva (puntero, longitud) a todas partes. - El tamaño del buffer es tu trabajo. C no hará crecer tu buffer de salida, ni lo harán los decodificadores - la update de OpenSSL escribe lo que decodifica en el espacio que le diste. Dimensiona en
in_len * 3 / 4 + 3(más el overhead de envoltura si la entrada está envuelta y decodificas con un helper que no recorta) y mantén una comprobación de límite en cada envoltorio. - Búsquedas con char firmado. Si algún día escribes un decodificador tú mismo, el bug clásico es usar el byte de entrada como índice en una tabla de 256 entradas con un
charplano en una plataforma donde char es firmado: el byte0xFFse convierte en-1y indexas hacia atrás por la memoria. Indexa siempre con valoresunsigned charounsigned. - Los silenciosos son los peligrosos. APR-Util se detiene en el primer carácter inválido y no dice nada; GLib salta la basura y no dice nada. OpenSSL y Mbed TLS fallan a voces. Si tu entrada no es confiable, el silencio de la biblioteca es un bug en tu programa, no en la biblioteca.
- La línea de comandos se come los saltos de línea.
openssl base64 -ddecodifica cero bytes si la entrada no tiene salto de línea. Pipelines de shell que recortan los saltos de línea finales (tr -d '\n',xargs, guardados de editor sin salto de línea final) producirán salida vacía sin ningún error. - int contra size_t. La API one-shot de OpenSSL toma una longitud
int, APR-Util usaintpor todas partes, y las APIs de Mbed TLS y GLib usansize_t. La aritmética de longitudes mezcladas entre ellas es donde las advertencias de firmado/no firmado esconden bugs reales - y donde vive el techo de 2 GB de APR. - El espacio en blanco no es uniforme. OpenSSL salta todo el espacio en blanco en cualquier sitio; Mbed TLS permite CRLF/LF entre grupos y espacios justo antes de un salto, pero no después de uno ni a mitad de línea; las herramientas CLI varían. Un payload que es válido para un decodificador puede ser inválido para otro, y "funcionaba en mi máquina" suele significar "mi decodificador era más perezoso".
Buenos hábitos, reunidos
Valida antes de confiar: una comprobación de forma (caracteres del alfabeto, como máximo dos pads finales) caza la basura obvia antes de cualquier decodificación, pero solo una decodificación de verdad entiende la semántica de Base64, así que el decodificador estricto tiene la última palabra. Usa el par de streaming de OpenSSL cuando necesites longitudes honestas o entrada en trozos, y la one-shot cuando el payload es pequeño y corriges su longitud al instante. Mantén los pares (puntero, longitud) juntos y no dejes nunca que un buffer decodificado se encuentre con una función de cadenas. Compara el material de autenticación con CRYPTO_memcmp. Olfatea los bytes mágicos antes de creer un nombre de archivo o un tipo MIME declarado. Y trata a Base64 como lo que es - un formato de empaquetado, una cajita para bytes - no como un candado: nada de estas 64 letras hace que tus datos sean privados.
Una breve historia de Base64 en C
La historia empieza con el correo. En 1990 y 1991 un grupo de criptógrafos esbozó Privacy Enhanced Mail, un sistema para correo firmado y cifrado, y necesitaban una forma de llevar binarios a través de una red de 7 bits. Su respuesta, estandarizada como la RFC 1421 en 1993, codificaba datos a seis bits por carácter - "base 64" - en líneas de 64 caracteres, y la implementación era, por supuesto, C. Alrededor de ese mismo tiempo llegó la web con su propio MIME, la RFC 1521 (1993) y luego la RFC 2045 (1996), que mantuvo el mismo alfabeto, relajó la longitud de línea a 76 y convirtió a Base64 en el formato de adjuntos del joven internet.
La biblioteca estándar de C se perdió el barco entero. El estándar C89 se publicó en 1990, tres años antes que MIME, y el comité del idioma nunca ha añadido una función Base64 desde entonces - ni en C99, ni en C11, ni en C23 (la revisión de 2024). Así que el ecosistema creció alrededor de las bibliotecas: OpenSSL lleva las rutinas de codificación/decodificación EVP en libcrypto desde que cualquiera enlaza OpenSSL para TLS, Mbed TLS (renombrado de PolarSSL en 2015) mantuvo un par pequeño y estricto para sistemas embebidos, APR-Util se envió con Apache cuando el servidor necesitaba decodificar sus propias cabeceras de autenticación, y GLib añadió su trío para el escritorio. Los estándares persiguieron a las implementaciones: la RFC 3548 en 2003 ordenó las definiciones viejas, y la RFC 4648 en 2006 (Base-N Encodings) formalizó los alfabetos, la variante URL-safe y las reglas de seguridad en las que se apoya este artículo. Con razón, la sección 11 de esa RFC apunta a una implementación de referencia ISO C99 - el propio decodificador de ejemplo del estándar está escrito en C, lo cual te dice todo sobre dónde vive este formato.
Datos curiosos, edición C
Unos pocos datos con sabor a C que son simplemente divertidos de saber:
- El nombre es matemáticas, no marketing: cada carácter de salida lleva exactamente seis bits, y 2 elevado a 6 es 64. "Base64" es la radix, leída en voz alta.
- El alfabeto son 65 caracteres, no 64: los 64 símbolos más
=, que la RFC 4648 llama "el carácter 65 adicional", usado para una función de procesamiento especial. El pad es un trabajador, no una letra. - OpenSSL envuelve la salida codificada en 64 caracteres (la costumbre PEM) mientras coreutils envuelve en 76 (la costumbre MIME). La diferencia de 12 caracteres son dos décadas de historia del correo que puedes ver en la salida de dos comandos en la misma máquina.
- El autor del comando
base64de GNU coreutils es Simon Josefsson - la misma persona que escribió la RFC 4648. El estándar y una de sus implementaciones más usadas comparten autor, que es cómo los dos terminaron de acuerdo sobre cada caso límite. - Mbed TLS hace sus búsquedas en tablas a través de helpers de tiempo constante (
mbedtls_ct_base64_*), así que la velocidad de decodificación no filtra qué caracteres vio. Un detalle que nunca notarás y agradeces que exista. TQ==es el payload no trivial más pequeño: un byte real, dos pads. Es el vector de prueba perfecto - el decodificador one-shot de OpenSSL devuelve tres bytes para él, su decodificador de streaming devuelve uno, Mbed TLS devuelve uno, y GLib devuelve uno. Cuatro bibliotecas, dos respuestas, y la diferencia es el padding de ceros.- Las funciones base64 de APR son las únicas de este artículo que se preocupan por EBCDIC, porque httpd sigue corriendo en máquinas donde las letras no son ASCII. La biblioteca estándar de C nunca conoció un mainframe; APR sí.
- El payload vacío es la identidad universal: cada biblioteca codifica y decodifica una entrada de longitud cero a una salida de longitud cero, sin ningún error. Si tu decodificador se atraganta con una cadena vacía, tienes un bug, no un formato.
Pasar al lado del codificador
Ese es el lado del decodificador, y es donde vive la mayor parte del dolor, porque decodificar es donde te encuentras los datos de otras personas: sus decisiones de padding, sus saltos de línea, sus bytes corruptos, sus tokens. La dirección opuesta - convertir bytes en una cadena Base64 - es un animal más tranquilo con su propio reparto de trampas: la matemática exacta de buffers, la pregunta del envoltado de líneas y la factura de tamaño que le llega a cada emisor. La codificación Base64 en C se cubre a fondo en el artículo relacionado, enlazado desde esta página, y hace pareja con este de la misma forma que un decodificador hace pareja con un codificador: lee los dos y ninguna de las dos direcciones te volverá a sorprender.
Última actualización: 2026-09-08
Artículo relacionado: Codificación Base64 en C: una guía completa