Codificación Base64 en Rust: una guía completa
Estás a punto de enviar unos bytes por una puerta de solo texto, y el precio de entrada es una cadena de letras, dígitos, signos más y barras que es un tercio más larga de lo que tenías al empezar. Bienvenido a Base64, la caseta de peaje de internet. La página de inicio de este sitio explica el formato a fondo, así que solo toca redecir la forma: Base64 escribe tres bytes de entrada como cuatro caracteres tomados de un alfabeto de 64 símbolos, y una cola de uno o dos caracteres = le dice al lector dónde acabó el dato real. Ese intercambio de cuatro por tres es la economía entera del formato, y esta guía va de hacerlo bien en Rust.
Lo primero que hay que saber es que la biblioteca estándar de Rust no lo hará por ti. No hay ningún base64_encode() escondido en std, ni ningún use std::... que te cambie de idea. El ecosistema se decantó por una única crate llamada simplemente base64, y se ha convertido en carga estructural: la versión 0.23.1 salió el 4 de agosto de 2026, la crate ha publicado 45 versiones desde diciembre de 2015, y su contador de descargas ronda los 1.500 millones. Cada ejemplo de codificación de abajo usa esa única crate, más dos pequeños compañeros para el envolvimiento de líneas y el blindaje PEM.
La cadena de herramientas y la crate
Primero la cadena de herramientas, un comando por mundo:
# Debian / Ubuntu
sudo apt install rustc cargo
# o el instalador oficial, que configura rustup y cargo
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Después la crate, dentro de cualquier proyecto de cargo:
cargo new my-app
cd my-app
cargo add base64
Esa única línea es toda la instalación, y arrastra exactamente cero dependencias. La crate sale de fábrica con tres características opcionales que conviene conocer: std (activada por defecto; te da el streaming de std::io, los impls estándar de Error y la asignación en el montón), alloc (las API con asignación para builds incrustados no_std) y simd-unsafe (activada por defecto; los motores SIMD, que aparecen un par de secciones más abajo). La versión mínima soportada de Rust es 1.71.0, así que cualquier Rust reciente lo ejecutará. Alrededor se sientan los compañeros para lo que el núcleo decide deliberadamente no hacer:
- line-wrap (versión 0.2) inserta los saltos de línea de 76 o 64 caracteres que exigen MIME y PEM; la crate
base64en sí se niega a envolver, a propósito, como verás. - pem (versión 4) construye y analiza bloques
-----BEGIN ...-----para certificados y claves; depende debase64internamente y añade el blindaje y el envolvimiento. - base64ct (versión 1.8) es el decodificador en tiempo constante del proyecto RustCrypto, para cuando el lado de la lectura de un viaje de ida y vuelta es la mitad sensible.
- base64-turbo (versión 0.3) es un codec de alto caudal más nuevo que supera los 100 GiB/s en picos en hardware moderno.
Una codificación, cuatro caracteres
La ceremonia más pequeña posible se ve así, y ya demuestra todo el viaje de ida y vuelta:
use base64::prelude::*;
fn main() {
let packed = BASE64_STANDARD.encode("Hello, world!");
println!("{packed}");
// SGVsbG8sIHdvcmxkIQ==
let back = BASE64_STANDARD.decode(packed).unwrap();
println!("{}", String::from_utf8(back).unwrap());
// Hello, world!
}
Hay dos cosas que merecen una mirada. El módulo prelude te entrega dos cosas a la vez sin hacer ruido: el motor BASE64_STANDARD y el trait Engine cuyos métodos estás llamando, y por eso un simple use base64::prelude::*; es todo lo que este ejemplo necesita. Y encode() acepta cualquier cosa que se pueda leer como bytes, gracias a la cota AsRef<[u8]>: un &str, un literal &[u8], un Vec<u8>, lo que se te ocurra. La mitad de decodificación del ejemplo está ahí solo para vigilar al codificador, porque la dirección opuesta tiene su propia guía completa en el sitio hermano. Si quieres la prueba de humo de la propia crate, su documentación codifica asdf y obtiene YXNkZg== de vuelta; mismo alfabeto, misma matemática.
El precio exacto de cada byte
Cada codificador Base64 que exista cobra el mismo impuesto, y una vez que puedes ver la matemática, puedes presupuestarlo. Cada carácter de salida lleva 6 bits, cada byte de entrada lleva 8, y el montón más pequeño que es ambas cosas es de 24 bits: exactamente 3 bytes entran, exactamente 4 caracteres salen. Esa proporción es todo el espectáculo, así que un archivo de 3 kilobytes se convierte en 4 kilobytes y una subida de 10 megabytes en 13.3. El padding es el error de redondeo hecho visible: cuando la entrada no es un múltiplo de 3 bytes, el grupo final tiene capacidad sobrante, y el codificador la rellena con = para que la longitud de salida siga siendo un múltiplo de 4. Aquí está la tabla de verdad del RFC 4648, que el motor estándar reproduce exactamente:
| Entrada | Longitud mod 3 | Codificada | Longitud de salida |
|---|---|---|---|
"" (vacía) |
0 | "" (vacía) |
0 |
f |
1 | Zg== |
4 |
fo |
2 | Zm8= |
4 |
foo |
0 | Zm9v |
4 |
foobar |
0 | Zm9vYmFy |
8 |
use base64::prelude::*;
let words: [&[u8]; 4] = [b"", b"f", b"fo", b"foo"];
for input in words {
println!("{:?} -> {:?}", String::from_utf8_lossy(input), BASE64_STANDARD.encode(input));
}
// "" -> ""
// "f" -> "Zg=="
// "fo" -> "Zm8="
// "foo" -> "Zm9v"
Lee esa primera fila dos veces, porque es la que todos calculan mal en la cabeza: la entrada vacía codifica a la cadena vacía, no a AA==. La cadena AA== es la codificación de exactamente un byte, un NUL, que es una carga genuinamente distinta. Y cuando necesitas dimensionar un buffer antes de codificar, la crate te da la matemática como una const fn, así que hasta puedes dimensionar arrays en tiempo de compilación:
let padded = base64::encoded_len(15, true).unwrap();
let slim = base64::encoded_len(15, false).unwrap();
println!("{padded} / {slim}"); // 20 / 20
println!("{:?}", base64::encoded_len(13, true)); // Some(20)
println!("{:?}", base64::encoded_len(13, false)); // Some(18)
println!("{:?}", base64::encoded_len(14, false)); // Some(19)
println!("{:?}", base64::encoded_len(100, false)); // Some(134)
Observa las filas de 13 y 14 bytes, porque son las que tumban los cálculos a ojo: 13 bytes necesitan 18 caracteres sin padding pero 20 con, mientras que 14 bytes necesitan 19 y 20. La función devuelve un Option, que es None solo cuando la matemática de longitudes desbordaría, así que un unwrap() es seguro para cualquier entrada que pueda existir de verdad en memoria. Para el mundo con forma de correo el impuesto tiene un recargo: MIME envuelve las líneas a los 76 caracteres, y la vieja regla práctica dice que el Base64 envuelto cuesta unas 1,37 veces el tamaño original, más el sobrecoste de las cabeceras. El propio FAQ de la crate tiene una opinión menos educada sobre el padding en sí: los bytes = "no afectan a la decodificación salvo para dar la oportunidad de decir 'ese padding es incorrecto'", y "sin duda se han desperdiciado exabytes de almacenamiento y transferencia en bytes = sin sentido".
Padding: una decisión sobre el lector
En base64 0.23 las funciones de codificación desnudas están obsoletas - la forma actual es llamar a un método de un motor, y un motor es una política: qué alfabeto escribir y qué padding añadir. Los presets viven en base64::engine::general_purpose, con los cuatro populares reexportados al prelude:
| Motor | Alfabeto | Añade padding | Ideal para |
|---|---|---|---|
STANDARD / BASE64_STANDARD |
+ / |
sí | todo, el defecto |
STANDARD_NO_PAD / BASE64_STANDARD_NO_PAD |
+ / |
no | cargas esbeltas que además consumes tú |
URL_SAFE / BASE64_URL_SAFE |
- _ |
sí | contenido de URL que aun así quiere padding |
URL_SAFE_NO_PAD / BASE64_URL_SAFE_NO_PAD |
- _ |
no | JWTs, URLs, IDs de objetos |
Codificar sin padding no es un truco que la crate tolere; es una posición de primera clase, con las constantes de configuración NO_PAD y PAD preconfiguradas junto a los hermanos *_INDIFFERENT añadidos en la 0.23.0. Si los presets no encajan, construyes tu propio motor a partir de un Alphabet y un GeneralPurposeConfig con un solo dial, y como los motores son baratos de construir, guardas el resultado en una const en vez de reconstruirlo por petición:
use base64::engine::general_purpose::{GeneralPurpose, GeneralPurposeConfig};
use base64::prelude::*;
const SLIM: GeneralPurpose = GeneralPurpose::new(
&base64::alphabet::STANDARD,
GeneralPurposeConfig::new().with_encode_padding(false),
);
fn main() {
println!("{}", SLIM.encode("fo")); // Zm8
println!("{}", BASE64_STANDARD.encode("fo")); // Zm8=
}
Ahora la decisión se convierte en una decisión sobre los decodificadores de otras personas, y eso nunca es puramente estético. Las reglas de estrictividad del lado de la decodificación salen de DecodePaddingMode, y la tabla de abajo responde a "¿el otro lado puede leer lo que yo escribí?":
| Tú codificas con | Un decodificador estricto STANDARD |
Un decodificador STANDARD_NO_PAD |
Un decodificador INDIFFERENT |
|---|---|---|---|
STANDARD (con padding) |
lo lee | se niega al = |
lo lee |
STANDARD_NO_PAD |
se niega: falta padding | lo lee | lo lee |
URL_SAFE_NO_PAD |
se niega: alfabeto equivocado | se niega: alfabeto equivocado | lo lee solo con el alfabeto URL |
Las reglas prácticas salen de esa tabla. Si controlas los dos extremos, elige un motor y úsalo en todas partes, y prefiere sin padding para ahorrar bytes. Si consumes datos del mundo exterior, tu decodificador tiene voto en qué motor deberías emitir: un decodificador STANDARD de fábrica necesita tu padding, mientras que un decodificador STANDARD_PAD_INDIFFERENT acepta ambos. Y la elección tiene también un sabor de seguridad. Permitir tanto la grafía con padding como la sin padding de la misma carga hace que Base64 sea maleable; el paper de 2022 "La maleabilidad de Base64 en la práctica" (Chatzigiannis y Chalkias, ePrint 2022/361), al que enlaza la propia documentación de la crate, muestra por qué. Un protocolo donde los mismos datos pueden escribirse de dos formas distintas tiene la costumbre de sorprender al código que trata la cadena codificada como una identidad, así que cuando tu formato define una grafía canónica, aplícala en la frontera.
Base64url para tokens y enlaces
Las dos últimas letras del alfabeto del Base64 estándar son + y /, y en una URL esos son dos de los caracteres más caros del lenguaje: el signo más se convierte en %2B, barra en %2F, y el padding en %3D. La sección 5 del RFC 4648 lo arregla con el alfabeto seguro para URLs y nombres de archivo, que cambia a los dos problemáticos por - y _ y normalmente se salta también el padding. Los motores hacen la distinción imposible de pasar por alto:
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine;
let packed = URL_SAFE_NO_PAD.encode(b"\xfb\xef\xbe");
println!("{packed}"); // ----
let back = URL_SAFE_NO_PAD.decode(packed).unwrap();
println!("{back:02x?}"); // [fb, ef, be]
Tres bytes de la entrada más insoportable posible se convierten en una cadena de cuatro caracteres que puedes pegar en una URL, un nombre de archivo, una cookie o una clave de base de datos sin una sola escapada percent. Este es el alfabeto donde viven los JSON Web Tokens: un JWT son tres partes base64url unidas por puntos, y acuñar uno con la crate jsonwebtoken (versión 11 en 2026) se ve así:
use serde::Serialize;
use jsonwebtoken::{EncodingKey, Header, encode};
#[derive(Debug, Serialize)]
struct Claims {
sub: String,
company: String,
exp: u64,
}
let key = b"secret";
let my_claims = Claims {
sub: "b@b.com".to_owned(),
company: "ACME".to_owned(),
exp: 19_000_000_000, // bien dentro del futuro
};
let token = encode(&Header::default(), &my_claims, &EncodingKey::from_secret(key)).unwrap();
println!("{token}");
// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiQGIuY29t...
La versión 11 tiene un requisito de configuración que pica a los recién llegados: la crate necesita exactamente una de las características rust_crypto o aws_lc_rs activada en Cargo.toml, y si ninguna está activada hace pánico la primera vez que firmas o verificas un token. Fíjate en el campo exp del struct: la validación de la crate la trata como obligatoria por defecto, así que los tokens de verdad la llevan igualmente, y el alfabeto base64url dentro del token es asunto enteramente de la biblioteca. Si solo inspeccionas tokens en vez de acuñarlos, el artículo hermano muestra el asomo de cinco líneas. Y un recuerdo de la regla de oro, que se aplica con fuerza especial a los tokens: las tres partes de un JWT se leen todas sin clave. Base64 es un asiento junto a la ventanilla, no un candado.
Texto entra, bytes salen
Los codificadores no leen la mente, así que "codifica esta cadena" siempre significa "codifica los bytes UTF-8 de esta cadena" en Rust, porque eso es lo que entrega str::as_bytes(). La buena noticia es que la web moderna es casi enteramente UTF-8, así que el camino honesto es corto y feliz:
use base64::prelude::*;
let text = "café";
let packed = BASE64_STANDARD.encode(text.as_bytes());
println!("{packed}"); // Y2Fmw6k=
Todos los casos multibyte se comportan bien:
| Texto original | Base64 | Viaje de ida y vuelta |
|---|---|---|
café |
Y2Fmw6k= |
limpio |
日本語 |
5pel5pys6Kqe |
limpio |
😀 |
8J+YgA== |
limpio |
π ≈ 3.14159 |
z4Ag4omIIDMuMTQxNTk= |
limpio |
La única decisión de verdad es qué bytes usas de partida. Si los datos llegan como bytes y no como texto, un archivo leído de disco o un buffer de una llamada de red, sáltate la cadena por completo y codifica el Vec<u8> directamente; esa también es la única respuesta correcta para cargas que no son UTF-8, como un PNG o un protobuf. Deja cualquier imagen al lado de tu código y apunta la lectura a ella:
use base64::prelude::*;
let file_bytes = std::fs::read("sprite.png").unwrap();
let size = file_bytes.len();
let packed = BASE64_STANDARD.encode(file_bytes);
println!("{size} bytes -> {} base64 chars", packed.len());
// todo PNG codificado empieza por iVBORw0K
assert!(packed.starts_with("iVBORw0K"));
Esa última afirmación es una comprobación de cordura de regalo y uno de los prefijos más reconocibles de internet. Y si algún día codificas el mismo texto lógico a través de dos juegos de caracteres distintos, o codificas bytes que leíste mal como otro juego de caracteres, el viaje de ida y vuelta volverá como mojibake con cara de póker. El codificador nunca miente; simplemente codifica los bytes que le das, y eso es al mismo tiempo su mayor fortaleza y su única trampa.
Cuando un formato quiere líneas
La crate base64 deliberadamente no inserta saltos de línea, y no es la primera vez que toma esa decisión. La versión 0.5.0 salió con envolvimiento MIME de líneas integrado y finales de línea configurables, y la versión 0.10.0 lo retiró; la biblioteca decidió que el envolvimiento era demasiado de opinión para una crate general y complicaba la historia de no_std. Si un formato exige líneas, la crate line-wrap existe exactamente para eso. Su única función, line_wrap(), toma tu buffer preasignado, la longitud de la entrada, el límite de columna y el final de línea, y devuelve el número de bytes de finales de línea que insertó:
use base64::prelude::*;
let data = BASE64_STANDARD.encode(vec![b'a'; 300]); // 400 caracteres
let mut buf = vec![0u8; data.len() + 16];
buf[..data.len()].copy_from_slice(data.as_bytes());
let endings = line_wrap::line_wrap(&mut buf, data.len(), 76, &line_wrap::crlf());
buf.truncate(data.len() + endings);
let wrapped = String::from_utf8(buf).unwrap();
println!("{} chars in, {} bytes out, {} line endings", data.len(), wrapped.len(), endings);
// 400 chars in, 410 bytes out, 10 line endings (cinco pares CRLF)
Dimensiona el buffer con margen para los finales, llama a la función y recorta al total reportado; los cinco pares CRLF son el precio de la regla de 76 columnas de MIME. Para PEM, cambia el límite y el final, 64 columnas y line_wrap::lf(), y tienes el cuerpo de texto del blindaje. Después la crate pem añade las cabeceras en una sola llamada:
let pem_block = pem::encode(&pem::Pem::new("CERTIFICATE", b"0123456789abcdef"));
println!("{pem_block}");
// -----BEGIN CERTIFICATE-----
// MDEyMzQ1Njc4OWFiY2RlZg==
// -----END CERTIFICATE-----
let back = pem::parse(pem_block).unwrap();
println!("{}: {} bytes", back.tag(), back.contents().len());
// CERTIFICATE: 16 bytes
// y finales de línea al estilo Unix, si el consumidor es quisquilloso
let lf_block = pem::encode_config(
&pem::Pem::new("KEY", b"0123456789abcdef"),
pem::EncodeConfig::new().set_line_ending(pem::LineEnding::LF),
);
Por defecto, pem::encode usa CRLF, la convención histórica de PEM; el constructor set_line_ending cambia a LF para las herramientas que lo esperan. Fíjate en lo que la crate pem no hace: nunca llama a una función de base64 que tú puedas ver, porque la codificación es asunto suyo interno. Cuando un formato quiere líneas, la arquitectura es una crate por trabajo.
Streaming en espacio constante
Para datos demasiado grandes para guardar en una variable, la crate responde con la misma filosofía de streaming que el resto del io de Rust: el write::EncoderWriter envuelve cualquier escritor y codifica en base64 todo lo que le escribas, en espacio constante. El ritual completo para un buffer se ve así, y la estrella de la sección es la llamada a finish():
use std::io::Write;
use base64::prelude::*;
use base64::write::EncoderWriter;
fn main() {
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
encoder.write_all(b"the quick brown fox jumps over the lazy dog").unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
// dGhlIHF1aWNrIGJyb3duIGZveCBqdW1wcyBvdmVyIHRoZSBsYXp5IGRvZw==
}
¿Por qué finish() es la estrella? Porque es la única llamada que vacía el grupo parcial final y añade el padding, y el codificador tiene un método hermano que no lo hace. La propia documentación de la crate lo dice en claro: finish() "codifica los bytes de entrada restantes y añade padding si procede. Se llama automáticamente al desasignar (ver la implementación de Drop), pero cualquier error que ocurra al invocar el escritor subyacente se suprimirá. Si quieres manejar ese tipo de errores, llama a finish() tú mismo." La implementación de Drop se comporta como BufWriter: vacía, pero ignora los errores durante el drop. Así que el último grupo parcial no se pierde, pero "seguro que funcionó" no es una estrategia de envío, porque el error de escritura que habría contado es cosa del pasado.
El mismo stream funciona un nivel más alejado a través de io::copy cuando quieres todo el pipeline en una sola llamada, y hay un envoltorio de regalo para los momentos de "solo necesito esto dentro de una cadena de formato":
use std::io;
use base64::prelude::*;
use base64::write::EncoderWriter;
let file = b"the quick brown fox jumps over the lazy dog".to_vec();
let mut cursor = io::Cursor::new(file);
let mut encoder = EncoderWriter::new(Vec::new(), &BASE64_STANDARD);
io::copy(&mut cursor, &mut encoder).unwrap();
let packed = encoder.finish().unwrap();
println!("{}", String::from_utf8(packed).unwrap());
use base64::display::Base64Display;
use base64::prelude::*;
let value = Base64Display::new(b"\0\x01\x02\x03", &BASE64_STANDARD);
println!("base64: {value}"); // base64: AAECAw==
Ese envoltorio Base64Display es una joyita: formatea bytes como Base64 dentro de cualquier cadena de formato sin una sola asignación de montón, lo que de pronto hace agradables las líneas de log y la salida de depuración.
Asignación, y su ausencia
El método cómodo asigna, y para la mayor parte de tu vida ese es el intercambio correcto. Pero el trait Engine expone tres sabores de codificación, y la tabla de abajo es toda la matriz de decisión:
| Método | Salida | Asigna |
|---|---|---|
encode() |
un String nuevo |
siempre |
encode_string() |
añade a tu String |
solo si debe crecer |
encode_slice() |
escribe en tu &[u8] |
nunca |
use base64::prelude::*;
let input = b"Hello, world!";
let mut buf = vec![0u8; base64::encoded_len(input.len(), true).unwrap()];
let written = BASE64_STANDARD.encode_slice(input, &mut buf).unwrap();
buf.truncate(written);
println!("{}", std::str::from_utf8(&buf).unwrap()); // SGVsbG8sIHdvcmxkIQ==
// o guarda el buffer enteramente en la pila
let mut stack = [0u8; 24];
let n = BASE64_STANDARD.encode_slice(b"abc 123", &mut stack).unwrap();
println!("{}", String::from_utf8(stack[..n].to_vec()).unwrap()); // YWJjIDEyMw==
// y si lo dimensionaste mal, te llevas un error, no un desborde de buffer
let mut tiny = [0u8; 5];
println!("{:?}", BASE64_STANDARD.encode_slice(input, &mut tiny));
// Err(OutputSliceTooSmall)
Dimensiona el buffer con encoded_len(), escribe con encode_slice(), y si te equivocaste de tamaño te llevas un EncodeSliceError::OutputSliceTooSmall limpio en vez de comportamiento indefinido, y eso en un lenguaje de sistemas es la diferencia entre una tarde aburrida y una larga. Para trabajo incrustado las mismas funciones existen detrás de la característica alloc, así que puedes mantener la API y soltar el montón.
Velocidad: los motores SIMD
La versión 0.23.0, la que salió en julio de 2026, trajo la característica principal: motores acelerados por SIMD para los alfabetos estándar y URL-safe. Hay tres, y se dividen por cuánto se fían de tu hardware:
| Motor | Detecta en tiempo de ejecución | Funciona en no_std |
|---|---|---|
Simd |
sí, elige AVX2 o NEON, y retrocede al motor escalar | no, necesita std para la detección |
Avx2 |
no, asume que la CPU tiene AVX2 | sí, en objetivos x86_64 |
Neon |
no, asume que la CPU tiene NEON | sí, en objetivos aarch64 |
use base64::engine::general_purpose::GeneralPurposeConfig;
use base64::engine::{Avx2, Simd};
use base64::Engine;
let turbo = Simd::standard(GeneralPurposeConfig::new());
println!("{}", turbo.encode("simd works!"));
// c2ltZCB3b3JrcyE=
if let Some(fixed) = Avx2::standard(GeneralPurposeConfig::new()) {
println!("{}", fixed.encode("hello avx2")); // aGVsbG8gYXZ4Mg==
}
El constructor Simd hace su detección de CPU una sola vez y devuelve el mejor núcleo que encuentra, o el motor escalar si ninguno aplica, así que constrúyelo una vez en una const o al arrancar y reutilízalo; en hardware capaz es varias veces más rápido que el camino escalar, tanto para codificar como para decodificar. Una nota al pie honesta: el camino SIMD es el único lugar de la crate que toca unsafe, y por eso la característica se llama simd-unsafe. Apaga la característica y la crate entera vuelve a ser #![forbid(unsafe_code)], con el motor escalar haciendo su trabajo honesto. Si el caudal bruto es todo el punto, la crate base64-turbo empuja los límites más allá, con picos superiores a 100 GiB/s gracias a núcleos AVX512, AVX2 y NEON detrás de detección en tiempo de ejecución, y un respaldo escalar 100% seguro en todo lo demás. La crate base64 tiene licencia doble MIT/Apache-2.0, así que todo esto es gratis, incluida la velocidad.
Cuatro alfabetos más
El alfabeto del RFC es el defecto, pero la crate base64 trae cuatro más, cada uno un pequeño monumento a algún protocolo real que necesitó su propio giro:
| Alfabeto | El giro | Quién lo usa | abc 123 codifica a |
|---|---|---|---|
alphabet::CRYPT |
./ van primero, luego dígitos y letras, sin padding |
clásicos hashes de contraseñas Unix crypt(3) | MK7X612mAk |
alphabet::BCRYPT |
./ primero, luego letras, luego dígitos |
hashes de contraseñas bcrypt | WUHhGBCwKu |
alphabet::IMAP_MUTF7 |
una coma hace de barra, sin padding | nombres de buzón UTF-7 modificado de IMAP | YWJjIDEyMw |
alphabet::BIN_HEX |
un alfabeto cargado de puntuación que salta letras confusibles | BinHex 4, el viejo envoltorio de archivos Macintosh | B@*M)$%b-` |
use base64::engine::general_purpose::{GeneralPurpose, NO_PAD};
use base64::Engine;
let crypt = GeneralPurpose::new(&base64::alphabet::CRYPT, NO_PAD);
println!("{}", crypt.encode(b"abc 123")); // MK7X612mAk
let bcrypt = GeneralPurpose::new(&base64::alphabet::BCRYPT, NO_PAD);
println!("{}", bcrypt.encode(b"abc 123")); // WUHhGBCwKu
let imap = GeneralPurpose::new(&base64::alphabet::IMAP_MUTF7, NO_PAD);
println!("{}", imap.encode(b"abc 123")); // YWJjIDEyMw
Misma entrada, tres salidas distintas, todas válidas Base64 en su propio dialecto. El alfabeto crypt es el que tiene un superpoder de verdad: como sus símbolos están ordenados para casar con los patrones de bits, ordenar las cadenas codificadas te da el mismo orden que ordenar los bytes originales, y por eso GEDCOM 5.5 (1996) lo usó para sus campos de multimedia - la revisión 5.5.1 retiró la característica - y la crate sigue trayéndote el alfabeto. Y si el dialecto que necesitas no está en la crate, puedes definirlo con una cadena de 64 caracteres, porque Alphabet::new() construye las tablas de codificación y decodificación por ti:
use base64::alphabet::Alphabet;
use base64::engine::general_purpose::{GeneralPurpose, PAD};
use base64::Engine;
// un base64 de mundo bizantino: +/ al frente en vez de al final
let alphabet = Alphabet::new(
"+/ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
).expect("a valid 64 char alphabet");
let bizarro = GeneralPurpose::new(&alphabet, PAD);
println!("{}", bizarro.encode(b"hello 99")); // YETqZE6eMRi=
// mientras que el motor estándar dice:
println!("{}", base64::prelude::BASE64_STANDARD.encode(b"hello 99"));
// aGVsbG8gOTk=
Una advertencia sobre el camino del alfabeto a medida: en el momento en que inventas un dialecto, te conviertes en la única persona del mundo que puede leer tus datos, así que hazlo solo cuando un protocolo lo exija, y escribe un comentario diciendo cuál.
Dónde trabajan los codificadores
La codificación Base64 aparece en los proyectos de Rust en un reparto predecible de situaciones:
- Subidas de archivos en APIs JSON, donde el archivo es un campo de bytes con un disfraz de texto, el uso más común por un margen amplio.
- Data URIs en HTML y CSS, del tipo
data:image/png;base64,..., maravillosos para iconos diminutos, cuestionables para imágenes de portada. - JWTs y OAuth, donde base64url es el dialecto y la crate
jsonwebtokenes la herramienta. - Bloques PEM para certificados y claves, las secciones
-----BEGIN CERTIFICATE-----que envuelven Base64 a 64 caracteres por línea. - Binarios en XML y archivos de configuración, el patrón
<data encoding="base64">que todavía encuentras en marcadores exportados y volcados de ajustes. - LDAP y archivos LDIF, que usan Base64 para mantener los valores de atributos binarios en una sola línea.
- Cargas de QR y pases por el portapapeles, donde el texto sobrevive el viaje y el binario no.
- Cabeceras de autenticación básica HTTP, donde
Basic TWFuOnBhc3M=es un par de credenciales, y un recuerdo de que esto es un problema de empaquetar, no de esconder.
Y la regla de oro que lo gobierna todo: Base64 es cinta de empaquetar, no un candado. No es cifrado y no es compresión - es lo contrario de la compresión, y cualquiera con este artículo puede revertir todo lo que hace en una línea. Codifica a gusto, pero nunca codifiques una contraseña, una clave de API o un secreto y lo llames protegido. Si tiene que esconderse, usa cifrado de verdad, y si es grande, considera si una subida multipart simplemente habría sido más barata que el impuesto.
Una década de pasos pequeños
El formato es más viejo que la web. En 1987, el protocolo Privacy-Enhanced Mail (RFC 989) necesitaba llevar datos binarios por canales de correo de 7 bits, y estandarizó esta codificación con líneas de exactamente 64 caracteres. Cada bloque -----BEGIN CERTIFICATE----- de internet es un descendiente de esa decisión, y por eso los archivos PEM todavía envuelven a los 64 hoy. En 1996 la especificación MIME (RFC 2045) adoptó el esquema, lo bautizó como "base64" por su alfabeto de 64 caracteres, y movió el envolvimiento a 76 caracteres. Antes de todo eso, las máquinas Unix salían de fábrica con uuencode y las Mac con BinHex, cada una con su propio alfabeto, y ambas todavía asoman en sistemas viejos como fósiles con cabeceras de archivo. En 2006, el RFC 4648 se convirtió en el estándar que todos citan, con las tablas de alfabetos, la variante base64url, y las reglas de codificación canónica que cada motor de este artículo implementa. Su sección 3.5 exige a los codificadores que pongan los bits finales sin usar a cero, y la crate lo hace; si tu carga más adelante hace saltar la comprobación InvalidLastSymbol de un decodificador estricto, la corrupción pasó aguas arriba.
La historia de la crate rima con ella. Apareció en crates.io en diciembre de 2015, y la versión 0.5.0 añadió con orgullo el envolvimiento MIME de líneas con finales de línea configurables. Después la versión 0.10.0 en 2018 retiró el envolvimiento y el manejo de espacios en blanco, la biblioteca decidió que una crate de propósito general debía codificar y dejar la poesía a la capa de aplicación; la misma versión añadió el EncoderWriter de streaming. La versión 0.20.0 en 2022 introdujo la abstracción de motores y convirtió el padding canónico en el defecto, y la 0.21.0 dejó obsoletas las viejas funciones sueltas en favor de los métodos de motor, con la nota del compilador "Use Engine::encode" (siguen funcionando, y por eso bastante código legado compila feliz). En 2024, la versión 0.22.0 afinó la semántica de los errores y aceleró la decodificación un 5 a 10 por ciento. Y en julio de 2026, la versión 0.23.0 llegó con los motores SIMD, los símbolos de padding personalizados, un mensaje de error más claro y el aumento del MSRV a 1.71, con el parche 0.23.1 del 4 de agosto arreglando la suite de pruebas para arquitecturas sin SIMD.
Cosas que valen una sonrisa
Porque una guía completa debería terminar con una sonrisa:
- La palabra "base64" codifica a
YmFzZTY0. Un formato que se describe a sí mismo es el equivalente técnico de un espejo que habla en Morse. - La cadena vacía codifica a la cadena vacía. Nada es la única entrada que no cuesta nada, y eso es una especie de exención fiscal.
AA==no es la codificación de nada; es la codificación de un byte NUL. En Base64, "nada" y "un cero" son criaturas distintas, y los decodificadores las distinguen.- Cada PNG codificado en Base64 empieza por
iVBORw0K. Ese es el número mágico del PNG con su cinta de empaquetar, uno de los prefijos más reconocibles de internet. - En una URL, los caracteres del Base64 estándar necesitan disfraces de escapada: más se convierte en
%2B, barra en%2F, y el padding en%3D. Base64url existe para que los caracteres puedan llevar su propia cara. - Los IDs de video de YouTube son base64url sin padding: ocho bytes de ID se convierten en la cadena de once caracteres que puedes pegar en cualquier parte. Uno de los usos más visibles del modo sin padding de todo internet.
- El viejo alfabeto de contraseñas de crypt(3) ordena correctamente: las cadenas codificadas ordenadas quedan en el mismo orden que el texto plano ordenado. GEDCOM 5.5 (1996) usó ese alfabeto para sus campos de multimedia, la revisión 5.5.1 retiró la característica, y la crate sigue trayéndotelo.
- BinHex, el viejo envoltorio de Macintosh, construyó su alfabeto para excluir caracteres confusibles visualmente como
7,O,gyo. Un codificador diseñado para ojos humanos, en un mundo antes del corrector ortográfico. - El propio FAQ de la crate es tajante con el padding: sin duda se han desperdiciado exabytes de almacenamiento y transferencia en bytes
=sin sentido. La caseta de peaje cobra desde 1987. - Base64 no es cifrado. Si lo fuera, no podrías leer la salida de ningún ejemplo de este artículo. Es un asiento junto a la ventanilla, no una bóveda.
La versión corta
Elige tu motor según el camino que recorrerán los datos: BASE64_STANDARD para todo lo que además decodificas tú, los motores _NO_PAD cuando controlas los dos extremos y quieres los bytes de vuelta, URL_SAFE_NO_PAD para tokens y URLs, y un Alphabet a medida solo cuando un protocolo insiste. Dimensiona tus buffers con encoded_len(), haz streaming de las cosas grandes por EncoderWriter y cierra siempre con finish(), envuelve líneas con line-wrap y pem solo cuando un formato lo exija, deja que los motores SIMD hagan el trabajo pesado cuando puedas, y recuerda que el intercambio de cuatro por tres es el precio de pasar por la puerta de solo texto. Codifica todo, protege solo lo que necesita un candado de verdad. Y cuando necesites ir en la otra dirección, abriendo una cadena de vuelta a los bytes que empezaron el viaje, el artículo hermano cubre la decodificación en Rust, con su tabla de resultados completa de mensajes de error exactos.
Última actualización: 2026-09-08
Artículo relacionado: Decodificación Base64 en Rust: una guía completa