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

Codificación Base64 en C: una guía completa

Tienes bytes. Quizás sean un JPEG leído del disco, quizás un token, quizás una contraseña que un cliente está a punto de transmitir, quizás los bytes crudos de un archivo que algún pipeline espera dentro de un campo JSON. Y más adelante en el camino hay un canal que solo habla texto: una cadena JSON, una URL, el cuerpo de un correo, un archivo de configuración, una columna de base de datos que hace de texto. Ahí es donde entra la codificación Base64: reescribe cada tres bytes de datos crudos como cuatro caracteres de un alfabeto de 64 letras, así que el resultado es ASCII plano que sobrevive a cualquier pipeline de texto del planeta. La página de inicio de este sitio explica el formato por completo; este artículo trata de hacer el trabajo bien en C, donde - como de costumbre - el idioma no lo hará por ti.

El único número que guardar en la cabeza: codificar hace crecer tus datos. Tres bytes se vuelven cuatro caracteres, así que cada payload sale de tu programa un tercio más grande, y un poco más cada vez que se añaden saltos de línea. Ese es el impuesto, y no hay forma de esquivarlo - pero en C el impuesto tiene partida, porque asignas el buffer de salida tú mismo y el buffer debe ser exactamente lo bastante grande. Haz la matemática bien una vez y cada codificador de este artículo se vuelve predecible: sin overflows, sin underflows, sin preguntarte dónde caerá el siguiente byte. Luego está la caja de herramientas de la que elegir - OpenSSL, Mbed TLS, APR-Util, GLib - y la elección importa, porque cada una envuelve distinto, termina distinto y falla distinto.

Cuatro codificadores, cuatro personalidades

Las cuatro bibliotecas codifican el alfabeto estándar correctamente e idénticamente - mismos bytes en, mismos caracteres fuera, siempre. Las diferencias están en el empaquetado, y el empaquetado es donde se esconden los bugs de interoperabilidad. Aquí va el panorama:

Biblioteca Cabecera Estilo de salida Modo de fallo
OpenSSL (libcrypto) <openssl/evp.h> Sin saltos de línea; escribe un terminador NUL Prácticamente ninguno (solo asignación)
Mbed TLS <mbedtls/base64.h> Sin saltos de línea; terminado en NUL Código de buffer-demasiado-pequeño con el tamaño necesario
APR-Util <apr-1.0/apr_base64.h> Sin saltos de línea; añade un NUL Ninguno - confía en el tamaño de tu buffer
GLib <glib.h> Sin saltos de línea; terminado en NUL, asignado en heap Devuelve NULL (solo asignación)

Fíjate en lo que falta en la tabla: ninguna de ellas envuelve líneas por defecto. Es deliberado - la RFC 4648 dice que las implementaciones no deben añadir saltos de línea a menos que la especificación circundante lo pida explícitamente - y es un alivio, porque un salto de línea suelto dentro de una cadena JSON o una URL es un error, no una característica. El envoltado existe para correo y PEM, y cuando lo necesitas lo sacas de la vía de streaming de OpenSSL o envuelves tú mismo en cinco líneas (la sección de correo muestra las dos). Para elegir biblioteca: usa OpenSSL si ya la enlazas, Mbed TLS para builds embebidos donde cada kilobyte se discute, APR-Util dentro del ecosistema Apache, y GLib cuando el resto de tu programa ya es GLib. Instalación: libssl-dev (Debian/Ubuntu) o openssl-devel (Fedora/RHEL) o brew install openssl (macOS); libmbedtls-dev para Mbed TLS; libaprutil1-dev más libapr1-dev para APR-Util; glib2.0-dev para GLib.

Haz la matemática antes de asignar

Antes de cualquier código, la aritmética, porque C no te salvará de un buffer demasiado pequeño. Cada tres bytes de entrada producen exactamente cuatro caracteres de salida. Si la longitud de entrada no es múltiplo de tres, el grupo final produce igualmente cuatro caracteres y las posiciones sin usar se marcan con pads de =: un byte de entrada se vuelve cuatro caracteres con dos pads, dos bytes de entrada se vuelven cuatro caracteres con un pad. Así que la longitud codificada exacta para n bytes es:

size_t encoded_chars(size_t n) {
  return ((n + 2) / 3) * 4;
}

Para 1000 bytes son 1336 caracteres; para 1 byte es 4; para 0 es 0. De ahí siguen dos ajustes. Primero, OpenSSL y Mbed TLS ambos añaden un terminador NUL después de los datos (y Mbed TLS reserva el espacio para él cuando le pides el tamaño), así que tu buffer quiere un byte extra: encoded_chars(n) + 1. Segundo, si quieres salida con líneas envueltas, añade un salto de línea por línea: el codificador de streaming de OpenSSL emite una línea de 64 caracteres por cada 48 bytes de entrada, así que la longitud envuelta es encoded_chars(n) + (n + 47) / 48. Verifica con 1000 bytes: 1336 caracteres más 21 saltos de línea son 1357, y eso es exactamente lo que produce el codificador. Escribe la fórmula una vez como función y úsala en todas partes; es la diferencia entre "cabe" y una corrupción de heap a las 3 AM.

size_t b64_buffer_size(size_t in_len) {
  return ((in_len + 2) / 3) * 4 + 1; /* caracteres + NUL */
}
size_t b64_buffer_size_wrapped(size_t in_len) {
  return ((in_len + 2) / 3) * 4 + (in_len + 47) / 48 + 1;
}

OpenSSL: un bloque o un grifo en marcha

La función one-shot de OpenSSL es la mula de carga, y es la más amable del grupo:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *text = "Mane";
  unsigned char out[32];
  int n = EVP_EncodeBlock(out, (const unsigned char *)text,
                          (int)strlen(text));
  printf("len=%d str=%s\n", n, (char *)out);
  return 0;
}

Escribe los caracteres codificados en out, añade un NUL después, y devuelve la longitud sin el NUL - así que imprimir con %s es seguro y la longitud está disponible si la necesitas. El buffer de salida debe caber encoded_chars(n) + 1 bytes. No hay camino de error que manejar: codificar no puede fallar, porque cualquier byte es entrada legal, y la función no tiene concepto de validación de entrada en el que tropezar. La única forma de que salga mal es que le des un buffer demasiado pequeño, y la sección de matemática es el antídoto.

El par de streaming es para cuando los datos son grandes o llegan a trozos. EVP_EncodeUpdate procesa la entrada en bloques de 48 bytes y escribe 64 caracteres más un salto de línea (65 bytes) por bloque completo, guardando cualquier resto en el contexto hasta más datos o la llamada final:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  if (ctx == NULL) {
    return 1;
  }
  EVP_EncodeInit(ctx);
  unsigned char in[1000];
  for (int i = 0; i < 1000; i++) {
    in[i] = (unsigned char)(i % 251);
  }
  unsigned char out[1400]; /* 1336 caracteres + 21 saltos de línea + margen */
  int outl = 0;
  int total = 0;
  EVP_EncodeUpdate(ctx, out + total, &outl, in, 1000);
  total += outl;
  EVP_EncodeFinal(ctx, out + total, &outl);
  total += outl;
  int nl = 0;
  for (int i = 0; i < total; i++) {
    if (out[i] == '\n') nl++;
  }
  printf("encoded 1000 bytes into %d chars, %d newlines\n",
      total, nl);
  EVP_ENCODE_CTX_free(ctx);
  return 0;
}

La salida de ese programa son 1357 bytes con 21 saltos de línea - la fórmula de la sección de matemática, hecha realidad. Dos notas prácticas. La nota de versión: desde OpenSSL 1.1.0 (2016) el tipo de contexto es opaco, así que asigna con EVP_ENCODE_CTX_new() y libera con EVP_ENCODE_CTX_free(); el patrón de pila más viejo EVP_ENCODE_CTX ctx; que encontrarás en tutoriales que apuntan a 1.0.2 o anteriores no compila contra cabeceras modernas, OpenSSL 3.x incluido. Y la nota de diseño: como solo se emiten bloques completos de 48 bytes desde EVP_EncodeUpdate, el pipeline fragmentado más limpio le da múltiplos de 48 - entonces cada línea que la función escribe es una línea terminada, y EVP_EncodeFinal solo decide cómo se envuelve la cola. Si tu entrada llega en tamaños arbitrarios (una lectura de red), el contexto igual maneja el alineamiento por ti; la costumbre de múltiplos de 48 es solo lo que hace la salida predecible.

Mbed TLS: pregunta, luego codifica, obtén una cadena

La codificación de Mbed TLS tiene el contrato más limpio del grupo, construido alrededor de una consulta de tamaño que puedes llamar con un destino NULL:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <mbedtls/base64.h>
int main(void) {
  const char *text = "Mane";
  size_t slen = strlen(text);
  size_t needed = 0;
  int rc = mbedtls_base64_encode(NULL, 0, &needed,
      (const unsigned char *)text, slen);
  if (rc != MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL) {
    printf("size query failed: %d\n", rc);
    return 1;
  }
  printf("needs %zu bytes\n", needed);
  unsigned char *out = malloc(needed);
  size_t olen = 0;
  rc = mbedtls_base64_encode(out, needed, &olen,
      (const unsigned char *)text, slen);
  if (rc != 0) {
    printf("encode failed: %d\n", rc);
    free(out);
    return 1;
  }
  printf("olen=%zu str=%s\n", olen, out);
  free(out);
  return 0;
}

Lee los detalles con cuidado, porque son una clase magistral de API amable. La consulta de tamaño reporta needed como los caracteres codificados más uno por el NUL - para "Mane" es 8 más 1, es decir, 9 - y se señala a sí misma con el código "buffer demasiado pequeño" (MBEDTLS_ERR_BASE64_BUFFER_TOO_SMALL, que es -0x002A) porque un destino NULL es, por definición, demasiado pequeño. La llamada de verdad entonces escribe los caracteres y el NUL, y *olen vuelve como 8 - la longitud sin el terminador - así que el buffer ya es una cadena C imprimible. Si le das un buffer que le falta un byte, obtienes de vuelta el mismo código "demasiado pequeño" con el tamaño necesario en *olen, así que el fallo te dice exactamente cuánto te faltó. Una nota más: la biblioteca hace sus búsquedas en tablas a través de helpers de tiempo constante, un pequeño toque de cuidado que no verás en la mayoría de los codificadores.

APR-Util y GLib: las otras dos

El codificador de APR-Util es un par plano de funciones con longitudes int:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <apr-1.0/apr_base64.h>
int main(void) {
  const char *text = "Mane";
  int needed = apr_base64_encode_len((int)strlen(text));
  char *out = malloc((size_t)needed);
  int n = apr_base64_encode(out, text, (int)strlen(text));
  printf("n=%d (includes NUL) str=%s\n", n, out);
  free(out);
  return 0;
}

Aquí apr_base64_encode_len() y el valor de retorno ambos cuentan el NUL, así que n es uno más que el recuento de caracteres - una diferencia de contabilidad frente a OpenSSL y Mbed TLS que ha producido bugs off-by-one en más de un código base. Aplica el mismo límite de longitud de 32 bits: para valores cerca o por encima de 2 GB, esta no es la herramienta. También está apr_base64_encode_binary(), que en máquinas EBCDIC omite la conversión de EBCDIC a ASCII de la entrada - en los mainframes donde esa conversión de otra forma ocurriría, y una diferencia no-op en todas las demás. Ese par, la variante binaria y las funciones de decodificación correspondientes son de hecho toda la superficie base64 que apr-util expone: no hay variante asignada desde el pool ni variante de streaming, así que el patrón malloc de arriba es el único patrón.

El codificador de GLib es el estilo asignado en heap - obtienes una cadena terminada en NUL y un deber:

#include <stdio.h>
#include <glib.h>
int main(void) {
  const char *text = "Mane";
  gchar *enc = g_base64_encode((const guchar *)text, strlen(text));
  printf("%s\n", enc);
  g_free(enc);
  return 0;
}

Sin envoltado, terminada en NUL, líbrala con g_free - la anotación G_GNUC_MALLOC en el prototipo es lo que les dice a los analizadores estáticos eso. Cuando sí quieres saltos de línea, el par incremental es la herramienta: g_base64_encode_step() toma un entero de estado y un flag break_lines y te dice cuántos bytes de salida escribió, y g_base64_encode_close() termina el grupo parcial final. Es la misma forma de máquina de estados que el par de streaming de OpenSSL, solo que con el estilo de parámetros de GLib.

Base64 URL-safe hecho a mano

Las cuatro bibliotecas de arriba hablan todas el alfabeto estándar: A-Z, a-z, 0-9, más y barra. La web, sin embargo, cada vez habla más el segundo dialecto de la sección 5 de la RFC 4648, llamado base64url: la misma codificación con + intercambiado por - y / intercambiado por _, y el padding final de = descartado cuando la longitud se conoce. Los JSON Web Tokens, los parámetros de estado OAuth e incontables IDs de API lo usan, porque + y / son ambos peligrosos en URLs mientras - y _ son caracteres no reservados que navegan a su aire. Como ninguna biblioteca de C emite este dialecto de forma nativa, lo haces tú - y son dos cambios pequeños, porque ya tienes un codificador de alfabeto estándar:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
static void to_base64url(const char *std_b64, char *out, size_t out_cap) {
  size_t i = 0;
  for (const char *p = std_b64; *p && *p != '='; p++) {
    char c = *p;
    if (c == '+') c = '-';
    if (c == '/') c = '_';
    out[i++] = c;
  }
  out[i] = '\0'; /* el padding se descarta a propósito */
}

El uso es de dos pasos - codifica estándar, luego traduce:

unsigned char enc[32];
EVP_EncodeBlock(enc, (const unsigned char *)"hi>there", 8);
char url_safe[32];
to_base64url((const char *)enc, url_safe, sizeof(url_safe));
printf("%s\n", url_safe); /* aGk-dGhlcmU */

Dos precauciones. El bucle se detiene en el primer =, que es lo que descarta el padding - no lo "arregles", descartar el padding es el punto (el receptor que lo necesite puede volver a añadirlo desde la longitud). Y dimensiona out para la longitud codificada completa, no para menos: la traducción es carácter por carácter hasta los pads, así que la capacidad que ya asignaste para la forma estándar es exactamente la correcta. Una nota honesta de interoperabilidad: si tus datos por casualidad no contienen bytes que mapeen a + o /, las formas estándar y URL-safe son idénticas y nada se quejará jamás de un mix-up - el bug solo aparece cuando los datos al fin contienen uno. Trata el dialecto como una propiedad del canal (URLs, tokens), no de los datos.

Texto y conjuntos de caracteres: UTF-8 son solo bytes

Una pregunta que sorprende a los novatos de C: ¿qué pasa con el texto acentuado, los emoji, los caracteres CJK? La respuesta es el dato más liberador de este artículo - no tiene que pasar nada. Base64 opera sobre bytes, y C es un idioma de bytes. Si tu texto es UTF-8 (lo que, en 2026, probablemente sea), la codificación UTF-8 de "café" son cinco bytes - 63 61 66 c3 a9 - y Base64 codifica esos cinco bytes exactamente como codificaría cualquier otro grupo de cinco bytes, produciendo Y2Fmw6k=. Sin parámetro de charset, sin BOM, sin paso de conversión, sin llamada a biblioteca. El codec no sabe ni le importa qué significan los bytes; ese es todo el diseño.

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *utf8 = "caf\303\251"; /* café en UTF-8 */
  unsigned char enc[32];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)utf8,
                          (int)strlen(utf8));
  printf("%.*s\n", n, (char *)enc); /* Y2Fmw6k= */
  return 0;
}

Dos trampas se sientan en los bordes de esta sección. La primera es wchar_t: si tus datos llegaron como caracteres anchos, primero debes convertirlos a una secuencia de bytes (en Linux, UTF-8, vía wcstombs() o tu mecanismo de locale) antes de codificar - Base64 de un array de wchar_t es la codificación de una representación interna, no del texto, y variará entre plataformas. La segunda es la codificación de origen: el literal de cadena en tu archivo C está codificado en la codificación del archivo de origen (UTF-8 en cualquier proyecto moderno), así que escribir "café" directamente funciona mientras el archivo de verdad sea UTF-8 y a tu compilador se le diga (así es, por defecto, en las toolchains modernas). Codifica los bytes que quieres enviar, y deja que el receptor se encargue de qué significan los bytes.

Imágenes: de un buffer a una cadena

El trabajo de codificación "real" más común en la web C: un archivo binario - un JPEG, un PNG, un icono - necesita viajar por un canal de texto, así que se vuelve una cadena Base64. La receta es leer el archivo en un buffer, dimensionar la salida con la fórmula, codificar y seguir. La mitad de leer el archivo merece cuidado, porque es donde los programas en C se rompen de verdad:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <openssl/evp.h>
int main(void) {
  FILE *f = fopen("photo.png", "rb");
  if (f == NULL) {
    return 1;
  }
  fseek(f, 0, SEEK_END);
  long size = ftell(f);
  fseek(f, 0, SEEK_SET);
  unsigned char *data = malloc((size_t)size);
  size_t got = fread(data, 1, (size_t)size, f);
  fclose(f);
  size_t out_cap = ((got + 2) / 3) * 4 + 1;
  unsigned char *enc = malloc(out_cap);
  int n = EVP_EncodeBlock(enc, data, (int)got);
  printf("png of %zu bytes becomes %d base64 chars\n", got, n);
  free(data);
  free(enc);
  return 0;
}

Notas: rb para leer binario - innegociable en cualquier plataforma, porque el modo texto puede traducir bytes y cambiar got; el par fseek/ftell para dimensionar (para pipes y sockets sin seek, lee en un buffer que crece en su lugar); y got en vez de size para la codificación, porque una lectura corta es una posibilidad real. Una imagen de 1 MB se vuelve unos 1.33 MB de texto - ese es el impuesto, cobrado por adelantado, y por eso un payload de imagen base64-dentro-de-JSON debería hacerte pausar y preguntarte si una subida de archivo de verdad habría sido más barata.

Archivos y la costumbre .b64

La otra cara del trabajo de imágenes: necesitas escribir la forma Base64 de un archivo en disco - un sidecar .b64, una copia de seguridad de un binario en un almacén seguro para texto, un adjunto para un mailer. Misma matemática, escritor distinto. La costumbre que vale la pena adoptar es escribir la salida de texto con saltos de línea explícitos a una longitud que el lado receptor espera - 76 para correo, 64 para consumidores estilo PEM, o ninguno en absoluto si el receptor es tu propio código:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  FILE *f = fopen("data.bin", "rb");
  if (f == NULL) {
    return 1;
  }
  fseek(f, 0, SEEK_END);
  long size = ftell(f);
  fseek(f, 0, SEEK_SET);
  unsigned char *data = malloc((size_t)size);
  size_t got = fread(data, 1, (size_t)size, f);
  fclose(f);
  unsigned char *enc = malloc(((got + 2) / 3) * 4 + 1);
  int n = EVP_EncodeBlock(enc, data, (int)got);
  FILE *out = fopen("data.b64", "w");
  for (int i = 0; i < n; i += 76) {
    int chunk = i + 76 < n ? i + 76 : n;
    fwrite(enc + i, 1, (size_t)(chunk - i), out);
    fputc('\n', out);
  }
  fclose(out);
  free(data);
  free(enc);
  return 0;
}

El bucle escribe líneas de 76 caracteres y una línea final más corta; un decodificador que salte el espacio en blanco (todos los serios lo hacen) no se fijará en la longitud de línea en absoluto, que es por lo que el receptor es el que hay que consultar, no tu gusto. Deja el archivo de salida en modo texto (w) en el lado de escritura si quieres finales de línea de la plataforma, o wb si el receptor cuenta caracteres con estrictez - y si cuenta, quiere exactamente lo que le prometiste: 76 caracteres más un salto de línea, nada más. Esa promesa, no los bytes, es lo que hace de un archivo .b64 un formato.

Data URIs: embebido de verdad

Las data URIs (la RFC 2397) son el otro lado de la llegada favorita del artículo de decodificación: en vez de recibir data:image/png;base64,..., tú construyes una. La forma es data:, el tipo de medio, ;base64, una coma, el payload - y construirla en C es un snprintf después de la codificación:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  /* la firma PNG de 8 bytes */
  const unsigned char png_sig[8] =
    { 0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n' };
  unsigned char enc[32];
  int n = EVP_EncodeBlock(enc, png_sig, 8);
  char uri[96];
  snprintf(uri, sizeof(uri), "data:image/png;base64,%.*s",
      n, (char *)enc);
  printf("%s\n", uri);
  return 0;
}

Tres notas de diseño. El tipo de medio que metes en la URI es una afirmación de la que eres responsable - olfatea primero los bytes mágicos del archivo real, o un data:image/png que lleva un JPEG confundirá a cada consumidor de una forma distinta. El flag ;base64 es obligatorio cuando el payload es Base64; omítelo y el payload debe ser en su lugar texto percent-encoded, que es otro formato por completo. Y la propia guía de la RFC es que las data URIs son para valores cortos: embeber un logo de 5 MB inline en una página HTML funciona, pero es un mal olor de diseño que una URL de asset de verdad arreglaría. La misma construcción aparece todo el tiempo en APIs JSON donde un cliente quiere un avatar en la misma petición que los datos del formulario - codifica, antepone, envía.

HTTP y JSON: payloads que sobreviven

La razón moderna más grande para codificar en C es JSON. Una cadena JSON es una secuencia de caracteres con reglas de escape, y los bytes crudos no encajan: un NUL a mitad de un literal de cadena es un problema de C, un salto de línea literal dentro de una cadena JSON es JSON inválido, y los bytes arbitrarios necesitan una historia de escape definida. Base64 esquiva el problema entero produciendo solo caracteres que JSON nunca tiene que escapar - los 64 caracteres del alfabeto más, en el dialecto estándar, =, ninguno de los cuales son comillas o backslashes. El binario entra como cadena y sale al otro lado exactamente como entró:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  unsigned char enc[64];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)"hello", 5);
  char json[160];
  snprintf(json, sizeof(json),
      "{\"avatar\": \"%.*s\"}", n, (char *)enc);
  printf("%s\n", json);
  return 0;
}

Eso imprime {"avatar": "aGVsbG8="} - un objeto JSON completo y válido, sin mecanismo de escape involucrado, y la precisión de %.*s mantiene la longitud exacta incluso si algún día cambias a un codificador que no termina en NUL. El coste honesto es el tamaño: cada byte que envías como cadena JSON te cuesta 4/3 de byte de aire más el nombre del campo y las comillas, así que un binario de 10 KB se vuelve una cadena de 13.3 KB dentro del JSON. Para blobs pequeños ocasionales (iconos, miniaturas, firmas, tokens) es un precio justo; para una subida de 500 MB es una arquitectura de la que te arrepentirás, y una subida de archivo de verdad es la herramienta para ese trabajo. También vale una línea: los + y / del alfabeto estándar son seguros dentro de una cadena JSON, pero si la misma cadena viaja después en una query de URL, no lo son - ese es el trabajo de la sección URL-safe.

JWTs: tres partes, un alfabeto

El consumidor insignia de base64url en C es el JSON Web Token. Un JWT compacto según la RFC 7519 es tres partes codificadas en base64url unidas por puntos - cabecera, payload, firma - y construir uno es un ejercicio agradable porque cada pieza es una función que ya tienes: codifica estándar, traduce a URL-safe, firma, repite. Aquí hay un token HS256 construido con el HMAC de OpenSSL:

#include <stdio.h>
#include <string.h>
#include <openssl/hmac.h>
#include <openssl/evp.h>
static void to_base64url(const char *std_b64, char *out, size_t out_cap) {
  size_t i = 0;
  for (const char *p = std_b64; *p && *p != '='; p++) {
    char c = *p;
    if (c == '+') c = '-';
    if (c == '/') c = '_';
    out[i++] = c;
  }
  out[i] = '\0';
}
int main(void) {
  const char *secret = "my-hmac-secret-key";
  const char *header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
  const char *payload = "{\"sub\":\"114365\",\"name\":\"Alice\"}";
  unsigned char hb[64], pb[64];
  EVP_EncodeBlock(hb, (const unsigned char *)header,
                  (int)strlen(header));
  EVP_EncodeBlock(pb, (const unsigned char *)payload,
                  (int)strlen(payload));
  char hu[64], pu[64];
  to_base64url((const char *)hb, hu, sizeof(hu));
  to_base64url((const char *)pb, pu, sizeof(pu));
  char signing_input[256];
  snprintf(signing_input, sizeof(signing_input), "%s.%s", hu, pu);
  unsigned char mac[EVP_MAX_MD_SIZE];
  unsigned int mac_len = 0;
  HMAC(EVP_sha256(), secret, (int)strlen(secret),
      (const unsigned char *)signing_input,
      (size_t)strlen(signing_input), mac, &mac_len);
  unsigned char mb[64];
  EVP_EncodeBlock(mb, mac, (int)mac_len);
  char mu[128];
  to_base64url((const char *)mb, mu, sizeof(mu));
  printf("%s.%s.%s\n", hu, pu, mu);
  return 0;
}

Dos cosas enseña la estructura. Primero, la entrada de firma son las dos partes URL-safe unidas por un punto - exactamente los bytes que el receptor verá - así que la traducción a base64url debe pasar antes de firmar, no después; firma la forma de alfabeto estándar y la verificación del receptor falla, que es un bug que compila, corre y parece un desajuste de claves. Segundo, la cabecera y el payload son JSON plano en un envoltorio Base64: cualquiera puede leerlos, y ese es el diseño. Un token es una nota firmada, no un sobre sellado - así que no metas en él nada que no te importe que un usuario interceptador lea, y nunca, nunca metas una contraseña en un payload JWT "porque está codificada". La parte Base64 de este trabajo es pequeña y aburrida, que es el mayor cumplido que puedes hacer a una implementación JWT.

HTTP Basic Auth: construyendo el token

La cabecera de autenticación más antigua es también el trabajo Base64 más simple: username:password, codificado en alfabeto estándar, después de la palabra Basic. Construirlo en C son dos líneas, y la única sutileza es que la contraseña puede contener un dos puntos (y la división en el lado receptor debe ser en el primero):

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *user = "alice";
  const char *pass = "s3cr3t";
  char creds[128];
  snprintf(creds, sizeof(creds), "%s:%s", user, pass);
  unsigned char enc[160];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)creds,
                          (int)strlen(creds));
  printf("Authorization: Basic %.*s\n", n, (char *)enc);
  return 0;
}

Eso imprime Authorization: Basic YWxpY2U6czNjcjN0. La advertencia de la RFC aplica también en el lado emisor: esto es codificación, no protección. Sobre una conexión HTTP sin cifrar, la credencial está a un base64 -d de cualquiera en el cable, así que la autenticación Basic es una costumbre exclusiva de HTTPS. (Las alternativas modernas - tokens bearer, mTLS - todas reutilizan el mismo mecanismo: ensambla una cadena, codifícala, métela en una cabecera. Base64 ha sido la forma de HTTP de colar datos estructurados a través de cabeceras de texto desde que el protocolo tenía cabeceras.)

Correo y PEM: donde vive el envoltado

El correo es la razón por la que el envoltado de líneas existe en absoluto. SMTP limita la longitud de línea, así que MIME topeó las líneas codificadas en 76 caracteres (PEM, su ancestro, en 64), y cada sistema de correo ha honrado ese tope durante treinta años. Si tu programa en C produce Base64 para el cuerpo de un correo o un adjunto, el envoltado no es cosmética opcional - una línea de 200 KB sin envolver será rechazada o destrozada por partes de la infraestructura de correo. El codificador de streaming de OpenSSL te da salida envuelta gratis (a su longitud heredada de 64 caracteres), y cuando necesitas exactamente 76, envolver un resultado one-shot es un bucle de cinco líneas:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  unsigned char enc[64];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)"hello world", 11);
  for (int i = 0; i < n; i += 76) {
    int chunk = i + 76 < n ? i + 76 : n;
    printf("%.*s\r\n", chunk, (char *)enc + i);
  }
  return 0;
}

Fíjate en \r\n: el correo quiere finales de línea CRLF, y si el texto codificado es una de varias partes MIME, todo lo que rodea al bloque base64 sigue las mismas reglas - longitudes de línea, finales CRLF, sin excepciones. Los archivos PEM (el formato de la mayoría de las claves y certificados) usan la misma idea con líneas de 64 caracteres entre marcadores -----BEGIN y -----END, y las herramientas de OpenSSL esperan ver esa armadura cuando vuelves a guardar una clave - así que si tu programa toca PEM, envuelve en 64 y guarda las etiquetas. En todas las demás partes - JSON, URLs, APIs, bases de datos - aplica la regla de la RFC y no envuelves en absoluto.

Colando valores por configuraciones y columnas

El caso de uso silencioso: los valores que romperían un formato de texto se empaquetan como Base64 para que no lo rompan. Un DSN de base de datos con punto y comas, una contraseña con comillas, un token con un salto de línea - la persona de ops los codifica una vez y el archivo de configuración nunca ve los caracteres problemáticos:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  const char *dsn = "pg:host=db;password=qu\"ote";
  unsigned char enc[128];
  int n = EVP_EncodeBlock(enc, (const unsigned char *)dsn,
                          (int)strlen(dsn));
  printf("DB_DSN_B64=%.*s\n", n, (char *)enc);
  return 0;
}

El programa imprime la línea exacta para pegar en un archivo .env, y el código C que luego lo lee es un getenv más un decodificar. Tres advertencias honestas, todas sobre lo que esto no es. No es cifrado: cualquiera que pueda leer el archivo de configuración puede decodificar el valor con una sola llamada, así que nunca empaquetes un secreto como Base64 y lo llames protegido. No es escape: si el formato necesita estructura preservada, una codificación de verdad (percent-encoding para URLs, escape JSON para JSON) es la herramienta correcta, y Base64 es para los valores que esos formatos no pueden expresar - los binarios. Y cuesta tamaño: un valor guardado en una columna TEXT de base de datos como Base64 ocupa alrededor de un 33 por ciento más de espacio que el original, que está bien para tokens y es un número real para columnas de archivo (para eso están las columnas BLOB).

Codificando desde la shell

Antes de echar mano de una invocación de cc, recuerda que ambas herramientas estándar codifican, y rápido. coreutils es el instrumento general: base64 codifica con envoltado de 76 caracteres por defecto, -w cambia la columna, y -w 0 desactiva el envoltado por completo:

base64 photo.png > photo.b64
base64 -w 0 photo.png > photo-oneline.b64
cat note.txt | base64 -w 0

La herramienta de OpenSSL es el mismo trabajo con empaquetado de linaje TLS: openssl base64 (el alias amable de openssl enc -base64) envuelve en 64 caracteres y -A lo cambia a una sola línea:

openssl base64 < photo.png > photo.b64
openssl base64 -A < photo.png > photo-oneline.b64

¿Por qué importarte la diferencia de envoltado? Porque las salidas por defecto de las dos herramientas no son intercambiables si un parser downstream cuenta caracteres - 76 por línea frente a 64 por línea es una diferencia visible en el archivo, y un parser que recorta el espacio en blanco no se fija mientras que uno que valida la longitud de línea absolutamente sí. Cuando tu programa en C es el productor y la shell la consumidora (o al revés), define el envoltado primero. Una nota de dialecto para sistemas con sabor a BSD: el flag de decodificación allá históricamente fue -D, y las versiones viejas de macOS todavía lo recuerdan; el lado de codificación es base64 en todas partes, que es la única dirección de la que trata esta sección de todas formas.

Streaming de lo grande

Codificar un archivo de varios gigabytes en un solo malloc es un problema de memoria que no necesitabas. La vía de streaming existe exactamente para esto, y la disciplina de bloques de OpenSSL hace el código casi trivial: dale a EVP_EncodeUpdate todo lo que el archivo te dé, deja que guarde el resto de cada bloque parcial de 48 bytes en el contexto, y escribe los 65 bytes de salida de cada bloque directo al archivo de destino. La memoria pico son tus dos buffers - unas decenas de kilobytes - sin importar lo grande que sea el archivo:

#include <stdio.h>
#include <string.h>
#include <openssl/evp.h>
int main(void) {
  EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
  if (ctx == NULL) {
    return 1;
  }
  EVP_EncodeInit(ctx);
  FILE *in = fopen("video.mp4", "rb");
  FILE *out = fopen("video.b64", "w");
  if (in == NULL || out == NULL) {
    return 1;
  }
  char inbuf[48 * 1024];            /* un múltiplo de 48: líneas completas */
  unsigned char outbuf[1024 * 65 + 65];  /* 65 bytes de salida por bloque de 48 bytes, más margen */
  size_t got;
  while ((got = fread(inbuf, 1, sizeof(inbuf), in)) > 0) {
    int outl = 0;
    EVP_EncodeUpdate(ctx, outbuf, &outl,
        (const unsigned char *)inbuf, (int)got);
    fwrite(outbuf, 1, (size_t)outl, out);
  }
  unsigned char tail[66];
  int outl = 0;
  EVP_EncodeFinal(ctx, tail, &outl);
  fwrite(tail, 1, (size_t)outl, out);
  EVP_ENCODE_CTX_free(ctx);
  fclose(in);
  fclose(out);
  return 0;
}

Dos detalles de diseño hacen trabajo en ese bucle. El buffer de entrada es un múltiplo de 48 bytes, así que cada llamada le da al codificador bloques completos y cada línea que escribe es una línea terminada de 64 caracteres; la llamada final entonces envuelve la cola verdadera. Si tu entrada viene en tamaños arbitrarios (un socket, un disco lento), el contexto igual absorbe el desalineamiento correctamente - la elección de múltiplo de 48 es sobre la predictibilidad de la salida, no sobre la corrección. El segundo detalle es el tamaño del buffer de salida: 65 bytes por cada 48 bytes de entrada más el margen del bloque final (66), que es la fórmula envuelta de la sección de matemática aplicada por trozo. El reporte de progreso es una línea - cuenta los bytes escritos a out contra el total que calculaste desde el tamaño del archivo - y como codificar hace crecer los datos, el archivo de salida aterrizará un 33 por ciento más grande que la entrada: cobra el disco por adelantado.

Los bordes afilados de la codificación

Las trampas, reunidas, todas con forma de C:

  • Off by one, en las dos direcciones. OpenSSL devuelve la longitud sin su NUL; la consulta de tamaño de Mbed TLS incluye espacio para el NUL; APR cuenta el NUL en sus longitudes. Tres bibliotecas, tres convenciones de contabilidad. Escribe la función de tamaño de buffer una vez (la sección de matemática) y deja de hacer aritmética en la cabeza en el punto de llamada.
  • El NUL es un byte que debes pagar. Cada codificador de este artículo quiere un byte extra en la salida para el terminador, y lo mismo cualquier buffer-append que escribas a mano. Un buffer dimensionado exactamente en encoded_chars(n) le falta un byte en el momento en que alguien quiere que printf("%s") funcione.
  • Codificar no puede fallar, así que no dejes que desborde. No hay ningún código de error que cace un buffer demasiado pequeño - el codificador escribirá con gusto más allá del final. El modelo de fallo de la codificación Base64 en C es enteramente tuyo: dimensiona bien, o corrompe memoria sin diagnósticos.
  • La doble codificación es el bug silencioso clásico. Un valor que ya es Base64, pasado por el codificador otra vez, produce una cadena Base64 perfectamente válida que decodifica a una cadena Base64 en vez de a los datos. El síntoma - "decodifica, pero a lo incorrecto" - se tarda una tarde en encontrar. Si un valor llega "pre-codificado", verifica que su longitud es múltiplo de cuatro y contiene solo caracteres del alfabeto antes de asumir que es dato crudo; si está codificado, salta la codificación.
  • El signo más en URLs. La salida de alfabeto estándar metida en una query string llega con el + convertido en espacio para cuando tu servidor analiza el formulario - el + es un espacio en percent/form encoding. Tokens e IDs que viajan en URLs quieren el dialecto URL-safe, punto.
  • Modo texto en el lado equivocado de la tubería. Leer un archivo binario en modo texto puede traducir bytes (en algunas plataformas) y cambiar tu longitud; escribir Base64 envuelto con la convención equivocada de finales de línea rompe receptores que cuentan caracteres. rb para entrada binaria, \r\n o \n explícitos donde una especificación lo exija, y nunca dejes que el runtime de C decida tus finales de línea en silencio.
  • Overflow de int en la matemática de tamaño. ((n + 2) / 3) * 4 en aritmética int desborda para entradas por encima de unos 1.5 GB, produciendo un "tamaño necesario" pequeño y positivo y un destrozo de heap. Haz la matemática en size_t (o uint64_t), que es también por lo que la API basada en int de APR tiene un techo de 2 GB del que no puedes librarte con ingeniería.
  • Envoltado donde el receptor no lo espera. La RFC 4648 dice: sin saltos de línea a menos que la especificación circundante lo pida. Un salto de línea dentro de un valor de cadena JSON es inválido; dentro de una URL es una petición distinta. Envuelve para correo, envuelve para PEM, y en ninguna otra parte.

La lista de comprobación corta

Calcula el tamaño del buffer con la fórmula, no con una suposición, y mantén una función de tamaño para todo el código base. Mantén (puntero, longitud) juntos incluso cuando el buffer está terminado en NUL, porque la longitud es el contrato y el NUL es una conveniencia. Elige el dialecto por el canal: estándar para JSON y cuerpos, URL-safe para URLs y tokens, envuelto para correo y armadura, sin envolver en todas las demás partes. Verifica los bytes mágicos antes de declarar un tipo MIME en una data URI. Nunca uses Base64 como cifrado, como sustituto del percent-encoding o como sitio para esconder un secreto - es una caja, no un candado. Y cuando los datos son grandes, hazlos streaming: los codificadores basados en bloques se diseñaron exactamente para eso, y la memoria constante es todo el punto.

Historia: cómo el empaquetado se estandarizó

La historia del codificador es la historia de las longitudes de línea. El primer Base64 fue un programa en C de principios de los 90. Privacy-Enhanced Mail (la RFC 1421, 1993) necesitaba llevar binarios por correo de 7 bits, y sus autores eligieron seis bits por carácter en líneas de 64 caracteres - el 64 es una reliquia de la tolerancia de longitud de línea de SMTP, y el código C hacía el empaquetado con búsquedas en tabla, una tras otra. Cuando MIME estandarizó el mismo alfabeto para la web (la RFC 1521 en 1993, la RFC 2045 en 1996), relajó la línea a 76 caracteres, y el mundo cargó dos costumbres - 64 y 76 - que ambas reclamaban ser "la" longitud de línea Base64. Los codificadores imitaron: la vía de streaming de OpenSSL se quedó con 64 (su herencia PEM), la herramienta de coreutils eligió 76 (su herencia MIME), y las dos herramientas en la misma máquina siguen en desacuerdo sobre dónde van los saltos de línea. El estándar finalmente tomó posición en 2006: la RFC 4648 dijo que las implementaciones no deben añadir saltos de línea en absoluto a menos que la especificación referenciante les diga explícitamente, que es por lo que cada biblioteca de este artículo pone salida sin envolver por defecto y por lo que el envoltado es ahora una característica opt-in para correo y armadura. El propio alfabeto, las reglas de padding y la regla de canonicidad de "los bits de padding deben ser cero" vienen de esas RFCs anteriores de PEM y MIME, y la 4648 las reexpone como las reglas canónicas de la familia. Y la sección 11 de la RFC apunta a una implementación de referencia - un programa ISO C99, alojado externamente porque el propio código "no podía incluirse en esta RFC por razones procedimentales" - otro recordatorio de que en este formato, C no es ciudadano de segunda. La biblioteca estándar de C, por su parte, nunca se ha puesto al día: C89 se congeló en 1990, antes de que existiera algo de esto, y C23 en 2024 todavía se envía sin una función Base64. Así que las bibliotecas que enlazas son el estándar, y la elección entre ellas es una decisión de diseño pequeña pero real - que es de lo que este artículo ha tratado.

Datos raros y pequeños

Unos datos que son simplemente divertidos, todos sobre el lado del empaquetado en C:

  • El "64" es la radix: cada carácter de salida es seis bits, y 2 elevado a 6 es 64. El formato nombra su alfabeto de la misma forma que C nombra sus enteros - por lo que el número de verdad es.
  • El bloque de 48 bytes del codificador de streaming de OpenSSL no es contabilidad arbitraria: 48 bytes de entrada son exactamente 16 grupos de 3, y 64 caracteres de salida son exactamente 16 grupos de 4. Los dos números son múltiplos de 16, que es la clase de redondez que hace felices al hardware y a las líneas de cache - o al menos hace felices a los humanos que leen el código.
  • Un byte de entrada se codifica a cuatro caracteres, dos de los cuales son =. El payload no vacío más pequeño posible es un 50 por ciento padding - la codificación más derrochadora del formato, y la que cada suite de pruebas usa porque es tan fácil escribirla mal.
  • Mbed TLS es el único codificador de este artículo que hace sus búsquedas en tiempo constante, porque la gente que escribe cripto embebida no confía en la indexación de tablas de tiempo variable ni en un codec que no es un cipher. La paranoia se transfiere.
  • La regla de codificación canónica - los bits de padding sin usar deben ser cero - suena trivial hasta que sabes que violarla significa que dos cadenas distintas pueden decodificar a los mismos bytes, lo cual rompe cada comprobación de "¿es esta cadena la codificación de ese archivo?" que exista. Tus codificadores todos cumplen; por eso base64 es una representación estable ante hash y puede sustituir a un nombre de archivo en un almacén de contenido.
  • El EVP_EncodeBlock de OpenSSL es una de las pocas funciones de C cuyo valor de retorno, su propia salida y su terminador NUL están de acuerdo: escribe n caracteres, un NUL, y devuelve n. En un idioma famoso por el off-by-one, eso es un momento de paz.
  • APR-Util es el único codificador de aquí que pregunta qué significa EBCDIC, porque Apache sigue corriendo en máquinas donde las letras están en un orden distinto que el de ASCII. En esas máquinas, "codificar" una cadena incluye reordenar primero su alfabeto en silencio.
  • La entrada vacía se codifica a la cadena vacía en cada biblioteca, sin pads y sin saltos de línea. El elemento identidad del formato, presente y correcto en las cuatro, lo cual lo convierte en la prueba unitaria más barata que escribirás jamás.

Pasar al lado del decodificador

Así que ese es el lado del empaquetado: la matemática, los cuatro codificadores, los dialectos y los sitios a donde van los bytes. Es la mitad tranquila del trabajo, porque codificar no tiene entrada inválida ni decodificador que discrepe contigo. La otra dirección - encontrarte con el Base64 del mundo exterior y recuperar los bytes - es donde se concentra el dolor: colas rellenas de ceros, truncamiento silencioso, alfabetos estrictos frente a tolerantes, y una línea de comandos que se come los saltos de línea finales. La decodificación Base64 en C se cubre a fondo en el artículo relacionado, enlazado desde esta página, y es la pareja natural de este: el codificador escribe la caja, el decodificador la abre, y entre los dos tienes cada trabajo Base64 que un programa en C se encontrará jamás.

Última actualización: 2026-09-08

Artículo relacionado: Decodificación Base64 en C: una guía completa