Decodificación Base64 en Ruby: una guía completa
En algún punto entre tú y los datos originales hay un muro de caracteres: letras mayúsculas y minúsculas, dígitos, tal vez un más o una barra o un guion, y quizá un signo igual aparcado al final. Tu editor no tiene idea de qué tipo de archivo es. Tu base de datos lo metió en una columna de texto. Llegó en una cabecera HTTP, una URL, una clave de YAML, o un ticket de soporte con un adjunto .b64. Lo reconoces en un instante - Base64 - y ahora necesitas los bytes de vuelta. En Ruby, esa necesidad está a un require y una llamada a método de distancia.
Por si el formato es nuevo para ti, aquí va la versión de treinta segundos. Base64 reescribe los datos crudos tres bytes a la vez: cada grupo de tres bytes se convierte en cuatro caracteres tomados de un alfabeto de 64 símbolos, y cuando la entrada no se divide exactamente entre tres, se añaden uno o dos caracteres = como padding para que la salida caiga siempre en un múltiplo de cuatro. Decodificar es el viaje de vuelta - cuatro caracteres entran, tres bytes salen - así que el resultado siempre es más pequeño que la entrada, unas tres cuartas partes de su tamaño. La página de inicio de este sitio repasa el formato al detalle, así que esta guía gasta su energía donde toca: en el lado de Ruby del trabajo.
La buena noticia: toda instalación de Ruby incluye el kit de decodificación completo. El módulo Base64 no necesita nada instalado, y sus tres decodificadores son tan pequeños que puedes leer su código completo en una sola sentada. La advertencia: el decodificador al que recurras primero es también el que nunca se queja, lo cual es una propiedad preciosa para el correo y una propiedad terrible para la seguridad. Al terminar esta guía sabrás exactamente qué acepta cada decodificador, cómo convertir en texto usable por Ruby los bytes que devuelve, y qué hacer con cada payload que un desarrollador Ruby decodifica de verdad - JWTs, cabeceras de autenticación, data URIs, cuerpos de correo, armadura PEM, archivos, blobs de configuración y gigantescos.
Conoce la caja de herramientas
Todo empieza con un require. No hay paso de instalación, no hay rarezas de plataforma, no hay extensión nativa que compilar:
require "base64"
puts Base64::VERSION
# => 0.2.0 en un Ruby 3.3 de serie, por ejemplo
Aquí está el lado de decodificación completo de la caja de herramientas, en una sola tabla, ordenada por la frecuencia con la que recurrirás a cada método:
| Decodificador | Tratamiento de los caracteres foráneos | Reglas del padding | Cuando algo sale mal |
|---|---|---|---|
Base64.decode64(str) |
ignora todo lo que no está en el alfabeto estándar, incluidos los saltos de línea y los espacios | lo que sea, hasta un padding incorrecto | nada - nunca lanza nada, solo devuelve lo que pudo decodificar |
Base64.strict_decode64(str) |
rechaza cualquier carácter fuera del alfabeto estándar | debe estar presente y ser exactamente correcto | lanza ArgumentError |
Base64.urlsafe_decode64(str) |
acepta el alfabeto URL-safe y el estándar, rechaza todo lo demás | opcional, pero si está presente debe ser correcto | lanza ArgumentError |
Si te gusta saber qué hacen tus herramientas por dentro, todo el lado de decodificación del módulo es un envoltorio fino alrededor de dos plantillas de la maquinaria pack/unpack del core, implementada en C dentro del core de Ruby:
# todo el lado de decodificación del módulo, condensado
def decode64(str)
str.unpack1("m")
end
def strict_decode64(str)
str.unpack1("m0")
end
La plantilla m es el lector indulgente, m0 es el estricto, y esa diferencia de un solo carácter explica todo el abismo de personalidad entre los dos primeros decodificadores. Como el trabajo pesado ocurre a velocidad de core, el módulo sigue siendo Ruby puro mientras devora megabytes en menos de diez milisegundos.
decode64: el camaleón
Base64.decode64 es el decodificador que le dice sí a todo. Si le das un payload limpio, lo decodifica. Si le das un blob estilo MIME lleno de saltos de línea, se encoge de hombros. Si le das una cadena que no es Base64 ni de lejos, te devuelve lo que haya podido exprimir, sin un solo aviso:
require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"
Esa segunda línea es toda su personalidad en un solo ejemplo. El decodificador salta todo lo que no forma parte del alfabeto estándar - saltos de línea, espacios, el raro carácter de control - y decodifica el resto. Ese es exactamente el comportamiento que se espera del Base64 MIME, por eso decode64 es la herramienta correcta para todo lo que ha viajado por correo.
La cara opuesta es lo que lo hace peligroso. Como el decodificador nunca se queja, tampoco te dice nunca cuándo la entrada estaba mal:
Base64.decode64("not base64 at all!")
# => diez bytes de basura con un aspecto perfectamente plausible
Base64.decode64("====")
# => ""
El primer ejemplo encuentra los caracteres que por casualidad son letras válidas del alfabeto, los decodifica, y te devuelve bytes a los que tal vez se te ocurra escribirlos tal cual en un archivo. El segundo ejemplo devuelve una cadena vacía para una cadena de cuatro caracteres de padding. No se lanza nada, no se registra nada. Si tu entrada no es de confianza, ese silencio es una función que quieres apagar - y para eso están los dos próximos decodificadores.
Un rasgo más que vale la pena conocer, porque es de esas cosas que se esconden en producción durante meses: la decodificación se detiene en el primer carácter =. Lo que hay después del padding no es un error; simplemente nunca se lee:
Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello" la parte "Zm9vYmFy" es invisible para el decodificador
strict_decode64: el guardián
Base64.strict_decode64 es el decodificador con su hoja de control. Solo acepta el alfabeto estándar (de A a Z, de a a z, de 0 a 9, más, barra), exige que el padding sea exactamente correcto, y se niega a producir un solo byte si se rompe alguna regla:
Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => lanza ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => lanza ArgumentError
La última línea es la que lo delata: el mismo payload que decode64 decodificó encantado ahora lanza por un solo salto de línea. Padding que falta, padding de más, un guion, un guion bajo, un espacio - todo es un delito, y todo el payload se hunde con él:
begin
Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
puts e.message
end
# => invalid base64
El guardián vigila hasta rincones del formato que no se te ocurriría revisar. Cuando una cadena Base64 termina con padding, algunos bits del último carácter nunca se usan, y el RFC dice que un codificador conforme debe poner esos bits a cero. Ruby lo verifica:
Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => lanza ArgumentError (los bits de padding no son cero)
La segunda cadena se decodificaría al mismo byte que la primera si el decodificador fuera descuidado. Ruby no es descuidado. En la práctica, esto hace de strict_decode64 el valor por defecto correcto para cualquier entrada que no hayas codificado tú: convierte los errores tipográficos, los recortes y el alfabeto equivocado en errores ruidosos y atrapables en lugar de una corrupción silenciosa.
urlsafe_decode64: el diplomático
Base64.urlsafe_decode64 existe para los payloads que viajan por sitios donde + y / son palabras reservadas: URLs, tokens, identificadores de base de datos. Internamente traduce el alfabeto URL-safe (guion y guion bajo) de vuelta al estándar, normaliza el padding y pasa el resultado al decodificador estricto:
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => lanza ArgumentError (quince caracteres necesitan un carácter de padding, no dos)
El primer ejemplo muestra su rasgo más útil: la entrada sin padding va bien. Si la cadena no tiene padding y su longitud no es un múltiplo de cuatro, el decodificador añade los caracteres = que faltan por ti - que es exactamente lo que producen los JSON Web Tokens, el mayor consumidor de Base64 URL-safe. Si el padding está presente, en cambio, debe ser correcto, igual que con el decodificador estricto.
Hay un rasgo que la documentación no grita: el diplomático habla ambos idiomas. Como el método reescribe los guiones y los guiones bajos antes de hacer una decodificación estricta, también acepta cadenas del alfabeto estándar:
Base64.urlsafe_decode64("aGVsbG8=")
# => "hello" el alfabeto estándar también es aceptado
Esa indulgencia es cómoda, pero significa que no puedes usar este método para saber de qué alfabeto viene un payload. Si eso te importa, inspecciona los caracteres tú mismo antes de decodificar.
Y a diferencia de decode64, el diplomático no tiene piedad con los espacios en blanco. Un salto de línea en cualquier parte de un payload URL-safe lanza ArgumentError, así que si tu entrada viene de un archivo con líneas dobladas, quita los saltos de línea antes.
Los bytes no son texto: el paso del encoding
Aquí está el paso que hace tropezar hasta a los desarrolladores experimentados, porque Ruby lo hace visible. Una cadena Base64 decodificada siempre lleva la etiqueta de encoding ASCII-8BIT (también llamado BINARY), sin importar si los datos originales eran un PNG, un payload de JWT o una carta de amor en UTF-8:
bin = Base64.decode64(Base64.strict_encode64("h\u{e9}llo"))
puts bin.encoding
# => ASCII-8BIT
puts bin.bytes
# => [104, 195, 169, 108, 108, 111]
Si el payload es binario - una imagen, un archivo zip, un hash - lo mantienes exactamente tal cual y lo escribes con File.binwrite. Sin conversión, sin preguntas. Si el payload es texto, los bytes casi seguro son UTF-8, y tienes que decírselo a Ruby:
text = Base64.decode64(payload)
text.force_encoding("UTF-8")
if text.valid_encoding?
puts text
else
puts "not valid UTF-8 after all"
end
Las dos llamadas hacen trabajos distintos. force_encoding solo reetiqueta los bytes; valid_encoding? luego verifica que formen UTF-8 de verdad. Ejecútalas en ese orden, porque validar primero una cadena BINARY no tiene nada que validar. Y una pequeña trampa de comparación para recordar para toda la vida: Ruby solo considera igual a una cadena BINARY con una cadena UTF-8 cuando ambas son ASCII puro, así que reetiqueta antes de comparar el texto decodificado contra tu original:
decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false mismos bytes, etiquetas diferentes
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true
JWTs: leer un token sin la clave
Un JSON Web Token es tres cadenas Base64 grapadas entre sí con puntos: cabecera, payload, firma. Las dos primeras son Base64 URL-safe sin padding de documentos JSON, lo que significa que un token lo puede leer cualquiera que lo vea - incluido tú, sin ninguna librería:
require "base64"
require "json"
token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0.dW5zaWduZWQ"
header_part, payload_part = token.split(".")[0, 2]
JSON.parse(Base64.urlsafe_decode64(payload_part))
# => {"sub"=>"1234567890", "name"=>"Alice"}
Para trabajo serio usarás la gem jwt, que se encarga de la parte que de verdad te protege - la firma - y de las validaciones de claims:
# Gemfile: gem "jwt"
require "jwt"
token = JWT.encode(
{ sub: "1234567890", name: "Alice", exp: Time.now.to_i + 3600 },
"my-secret-key",
"HS256"
)
payload, header = JWT.decode(token, "my-secret-key", true, algorithm: "HS256")
puts payload["name"]
# => Alice
Dos notas de seguridad corresponden aquí, porque ambas han costado incidentes reales a mucha gente. Primero, el payload no está cifrado; decodificarlo es leer, no descifrar, y la firma es la única protección, así que nunca trates un payload decodificado como entrada de confianza. Segundo, fija el algoritmo en JWT.decode exactamente como se muestra. Omitirlo deja que la propia cabecera del token decida cómo se verifica, y ese poco de flexibilidad es justo lo que explotan los famosos ataques de confusión de algoritmo en JWT.
Basic auth: la contraseña oculta a la vista de todos
La cabecera de autenticación más antigua de la web es el propio Base64. El Basic auth de HTTP envía las credenciales como user:password, codificadas, después de la palabra Basic - y la cabecera viaja en cada petición, así que aparece en cada log que vayas a depurar. Decodificar una es un trabajo de quitar y dividir:
require "base64"
header_value = "Basic YWxpY2U6czNjcjN0IQ=="
b64 = header_value.sub("Basic ", "")
decoded = Base64.decode64(b64)
user, password = decoded.split(":", 2)
puts user
# => alice
puts password
# => s3cr3t!
El límite de 2 en split importa: una contraseña puede contener legalmente dos puntos, y solo quieres cortar en el primero. La propia librería estándar de Ruby construye esta cabecera al revés, en Net::HTTP, usando directamente la plantilla pack del core:
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
Y la nota de seguridad que hay que decir aunque sea obvia: Base64 es un traductor, no un candado. El Basic auth solo es aceptable sobre HTTPS. El encoding existe para que las credenciales viajen por el cable como texto imprimible, no para que sean secretas.
Data URIs: la imagen que no es un archivo
Un data URI esconde un archivo entero dentro de una URL: un tipo de medio, la palabra base64, una coma y los bytes codificados. Los navegadores los renderizan en etiquetas img y en CSS, y a las aplicaciones HTML de archivo único les encantan porque no hay que hacer una segunda petición. Construir uno en Ruby lleva una línea:
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
Decodificar uno es lo inverso, con dos detalles que hacen tropezar. La coma es el separador, así que divide exactamente una vez, y la parte del tipo de medio puede ser lo que sea, inclusive nada:
data_uri = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
media_part, b64 = data_uri.split(",", 2)
puts media_part
# => data:image/png;base64
bytes = Base64.strict_decode64(b64)
File.binwrite("restored.png", bytes)
Usa strict_decode64 aquí, no decode64: el payload de un data URI es una sola línea limpia, y quieres un error ruidoso si está corrupto. También ten presente el impuesto de tamaño - cada imagen que incluyes en línea crece aproximadamente un tercio - así que los data URIs son perfectos para favicons, logotipos pequeños y fuentes, y una mala idea para fotos de cabecera.
Correo: líneas de sesenta caracteres y la gem mail
Base64 fue inventado para el correo, y las cicatrices se notan. SMTP fue diseñado para líneas cortas de texto de siete bits, así que el Base64 MIME dobla su salida en líneas cortas, y un decodificador conforme debe ignorar los saltos de línea. El decode64 de Ruby se comporta exactamente así, así que un cuerpo MIME doblado es comida fácil:
body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"
Rara vez lo escribirás a mano. La gem mail hace todo el trabajo MIME por ti: los adjuntos se codifican en Base64 automáticamente, las líneas se doblan a los 60 caracteres, cómodamente dentro del límite de 76 caracteres de MIME, y las cabeceras correctas van incluidas:
# Gemfile: gem "mail"
require "mail"
message = Mail.new do |m|
m.from = "dev@example.org"
m.to = "ops@example.org"
m.subject = "Binary report"
m.add_file("report.bin")
end
puts message.encoded
# la parte del adjunto lleva Content-Transfer-Encoding: base64
El mismo truco se esconde dentro de las cabeceras de correo. Una línea de asunto no ASCII llega como una palabra codificada RFC 2047: un charset, la letra B, y Base64 entre puntos de interrogación. Decodificar una a mano es un pequeño ejercicio de cirugía sobre cadenas:
header_value = "=?UTF-8?B?w7wgc2VjcmV0cw==?="
charset, kind, b64 = header_value.sub(/\A=\?/, "").sub(/\?=$/, "").split("?")
text = Base64.decode64(b64).force_encoding(charset)
puts text
# => ü secrets
PEM: claves y certificados con armadura
Las claves y los certificados pasan la mayor parte de su vida dentro de la armadura PEM: una línea BEGIN, un bloque de Base64 y una línea END. La armadura viene de los años 80 - Privacy-Enhanced Mail es donde empieza todo el linaje de Base64 - pero sigue siendo el formato que visten hoy tus archivos .crt y .key.
Decodificar un archivo PEM a mano es solo quitar la armadura y dejar que el decodificador indulgente mastique los saltos de línea:
require "base64"
pem = File.read("server.key")
body = pem.lines
.reject { |line| line.start_with?("-----") || line.strip.empty? }
.join
key_bytes = Base64.decode64(body)
Para uso real normalmente saltarás el paso manual y le pasarás la cadena PEM completa a OpenSSL, que lee la armadura por sí solo:
require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, o lo que sea que resulte ser la clave
El único detalle de interoperabilidad que vale la pena conocer: las líneas PEM son clásicamente de 64 caracteres, y el decodificador ignora los saltos de línea de todos modos, así que un doblado a 60 caracteres o una línea gigante decodificarán igual de bien.
Archivos y la convención .b64
El formato de archivo más común en el mundo Base64 es un archivo de texto plano con extensión .b64 (o a veces .base64) que contiene un solo payload codificado. Leerlo es un viaje de ida y vuelta en tres pasos:
require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)
Usa File.binwrite al salir - un PNG o zip decodificado es binario, y escribir en modo texto lo corrompería en plataformas que traducen los finales de línea. Si tu archivo .b64 vino de una herramienta que dobla líneas, decode64 se encarga de los saltos de línea gratis. Si prefieres validar en lugar de tolerar, lee el archivo en modo binario y quita los saltos de línea antes de una decodificación estricta:
encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)
La lectura en binario importa en Windows, donde el modo texto reescribe los finales de línea CRLF como LF - justo el tipo de mutación que no quieres que ocurra dentro de una cadena a punto de validar.
Base64 URL-safe: payloads que viajan en enlaces
Esta es la versión del decodificador sobre la variante URL-safe, porque la elección que hagas aquí cambia cuál de los tres decodificadores usarás. El Base64 URL-safe (RFC 4648, sección 5) intercambia los dos caracteres que a las URLs no les gustan - + pasa a ser -, / pasa a ser _ - y por lo general también descarta el padding. En Ruby te lo encontrarás en parámetros de consulta, valores de cookies, identificadores de APIs, IDs de vídeo estilo YouTube y, por supuesto, en JWTs.
Así se comportan los tres decodificadores con las mismas entradas, porque las diferencias son exactamente donde nacen los bugs:
| Entrada | decode64 | strict_decode64 | urlsafe_decode64 |
|---|---|---|---|
aGVsbG8= (estándar, con padding) |
"hello" |
"hello" |
"hello" |
aGVsbG8 (sin padding) |
"hello" |
ArgumentError |
"hello" |
SGVsbG8gd29ybGQ- (guion en el último grupo) |
"Hello world" (¡un byte menos!) |
ArgumentError |
12 bytes, la respuesta correcta |
aGVsbG8=\n (salto de línea final) |
"hello" |
ArgumentError |
ArgumentError |
aGVs!bG8= (un signo de exclamación suelto) |
"hello" |
ArgumentError |
ArgumentError |
La fila tres es la que pica. Un payload URL-safe decodificado con el decodificador estándar pierde en silencio su último byte en lugar de lanzar nada, porque decode64 simplemente ignora el guion. Si un payload puede venir de una URL, decodifícalo con urlsafe_decode64.
Una nota práctica: si alguna vez necesitas mover un payload URL-safe a un contexto que solo entiende el alfabeto estándar (una librería, un sistema ajeno), el truco clásico de interoperabilidad - traducir el alfabeto y añadir el padding tú mismo - son tres líneas:
def standardize_urlsafe(b64)
b64 = b64.tr("-_", "+/")
b64 += "=" * ((4 - b64.length % 4) % 4)
b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"
Rara vez lo necesitarás - urlsafe_decode64 ya hace el padding por ti - pero es el patrón que hay que reconocer en el código de otras personas, y el patrón al que recurrir cuando el alfabeto estándar es lo que el otro lado espera.
Configuración, variables de entorno y bases de datos
Base64 aparece en la configuración siempre que los datos binarios tengan que vivir dentro de un documento de texto. Un archivo .env, una configuración YAML o un blob de ajustes JSON no pueden transportar con seguridad bytes crudos, así que los bytes se codifican, y algo en tu aplicación tiene que decodificarlos al arrancar:
require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)
YAML recibe una mención especial, porque el formato tiene una etiqueta binaria nativa. Cuando vuelcas una cadena BINARY, Psych la escribe como un escalar !binary que contiene Base64, y al cargarla tus bytes vuelven intactos - sin encoding manual alguno:
require "yaml"
yaml_text = YAML.dump({ "logo" => File.binread("logo.png") })
puts yaml_text.lines.first(2)
# => "---"
# => "logo: !binary |-"
data = YAML.load(yaml_text)
puts data["logo"].encoding
# => ASCII-8BIT
En bases de datos la regla práctica es: si tu base de datos tiene un tipo binario de verdad, úsalo. Base64 en una columna TEXT es el patrón al que recurres cuando la capa de almacenamiento solo habla cadenas - algunos document stores, APIs con forma de JSON o un esquema legado que no puedes cambiar - y el precio es el impuesto de un tercio de tamaño sobre la columna, más la disciplina de decodificar al entrar y recodificar al salir en cada frontera.
Entradas grandes, memoria estable
El módulo es de tipo buffer: una llamada de decodificación lee la cadena entera de una vez y devuelve el resultado completo. No hay un decodificador en streaming en la librería estándar, así que el consejo honesto para payloads grandes es planificar la memoria. La buena noticia es que decodificar siempre hace las cosas más pequeñas - la salida es a lo sumo tres cuartas partes de la entrada - así que la cadena de entrada es tu única asignación grande.
Si un payload es lo bastante grande como para preocuparte, puedes decodificarlo en grupos de cuatro caracteres, porque los grupos de cuatro de Base64 son autosuficientes y el grupo parcial final lleva su propio padding:
require "base64"
def decode_in_chunks(b64)
b64.scan(/.{1,4}/).reduce("") do |result, group|
result + Base64.strict_decode64(group)
end
end
restored = decode_in_chunks(Base64.strict_encode64("a" * 1_000_000))
puts restored.length
# => 1000000
Esto funciona con entradas limpias y sin doblar - las mismas reglas que impone strict_decode64 - porque un grupo final suelto solo es válido con su padding presente. Para los archivos verdaderamente enormes, de varios gigabytes y cosas por el estilo, el patrón es leer el archivo en rebanadas, decodificar cada rebanada y volcar los bytes a disco en streaming, de modo que solo haya una rebanada en memoria a la vez.
One-liners para la terminal
No necesitas un archivo de script para decodificar algo en la shell. Ruby puede hacer require del módulo al vuelo:
ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world
Y para archivos, pasa la ruta del archivo en lugar del payload en sí:
ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin
Aquí viven dos trampas. Primera: si haces pipe a través de echo o cualquier comando de texto, un salto de línea final viaja contigo, y strict_decode64 lanzará una excepción por él - usa decode64 o haz chomp de la entrada:
echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'
Segunda: mantén print en lugar de puts para salida binaria, porque puts añade su propio salto de línea y corrompería el final de tu archivo restaurado.
Tampas que los desarrolladores Ruby realmente encuentran
- decode64 nunca lanza. Basura entra, basura sale. Si tu entrada no es de confianza y aceptas en silencio bytes corruptos, el bug saldrá a la luz semanas después en un archivo corrupto, no en la línea de decodificación. Por defecto, usa un decodificador estricto para todo lo que no hayas codificado tú.
- strict_decode64 y el salto de línea final. A los archivos de texto, a los pipes de echo y al copiar y pegar les encanta terminar con un salto de línea, y el decodificador estricto lanza
ArgumentErrorpor él. Hazchompde la entrada primero - o léela en modo binario y elimina los saltos de línea. - Olvidar el paso del encoding. Una cadena decodificada es BINARY hasta que digas lo contrario. Fuerza UTF-8 (y comprueba la validez) antes de tratar el resultado como texto, o recibirás mojibake y
Encoding::CompatibilityErroren el momento en que lo mezcles con cadenas UTF-8. - Comparar BINARY con UTF-8. Mismos bytes, etiquetas distintas, y
==dice false - a menos que la cadena sea ASCII puro. Reetiqueta antes de comparar. - Entrada URL-safe por el decodificador equivocado. Los guiones y los guiones bajos se descartan en silencio en
decode64, así que un payload URL-safe vuelve con un byte de menos y corrupto, sin ningún error. Usaurlsafe_decode64. - Los datos después del padding son invisibles.
decode64se detiene en el primer=. Genial para MIME, terrible para detectar un payload que fue truncado y luego repaddeado por otra herramienta. - El padding no canónico se acepta en silencio. Una cadena como
QR==lleva bits de padding que un codificador correcto habría puesto a cero;decode64la decodifica encantado mientrasstrict_decode64la rechaza. Nada te dirá nunca que tu codificador estaba mintiendo. - Las lecturas de archivo en modo texto en Windows reescriben los finales de línea antes de que los veas. Lee los archivos
.b64en modo binario si vas a validarlos.
Buenos hábitos para el lado de decodificación
- Elige el decodificador según la procedencia de los datos:
strict_decode64para todo lo que no sea de confianza (y rescue deArgumentErrorcomo tu rama de entrada inválida),urlsafe_decode64para payloads nacidos en URLs, ydecode64solo para formatos que son de verdad indulgentes, como cuerpos MIME. - En el momento en que los bytes se decodifican, decide su identidad: binario (mantén ASCII-8BIT, escribe con
File.binwrite) o texto (force_encodinga UTF-8, y luegovalid_encoding?antes de usar). - Nunca decodifiques y confíes. Un payload de JWT es legible precisamente porque es Base64; la firma decide si es real. Una cadena Base64 en un archivo de configuración es datos, no prueba.
- Cuando escribas validadores, ponlos a prueba con los casos aburridos: la cadena vacía, la entrada sin padding, la entrada doblada, la entrada URL-safe y el padding incorrecto. Esos son los casos que separan a los tres decodificadores.
Una breve historia de Base64 en Ruby
El módulo Base64 lleva más de quince años formando parte de la librería estándar de Ruby, y la forma en que se distribuye ha cambiado más de lo que esperarías:
- 2008, Ruby 1.8.7: el módulo llega con
encode64,decode64, más dos métodos que ya no existen -b64encode(doblado a una longitud de línea elegida) ydecode_b(decodificación de cabeceras de correo RFC 2047). Libros viejos e incluso algunas gems viejas los siguen mencionando, y llamar a cualquiera de ellos hoy es unNoMethodError. - 2009, la línea 1.9: llegan
strict_encode64,strict_decode64,urlsafe_encode64yurlsafe_decode64, y los dos métodos legados se jubilan (1.9.1 ya llevaba ambos cambios desde enero de 2009). - 2015, Ruby 2.3:
urlsafe_encode64gana la palabra clavepadding:, que te deja emitir salida sin padding para tokens y URLs. - 2020, Ruby 3.0: base64 se extrae de la librería estándar en su propia gem, versión 0.1.0, bajo el repositorio
ruby/base64. Se distribuye como default gem, así querequire "base64"sigue funcionando sin más. - 2023, Ruby 3.3: la versión 0.2.0 añade
Base64::VERSIONy un conjunto de documentación mucho más rico. - 2024, Ruby 3.4: la gem se reclasifica de default gem a bundled gem. La consecuencia práctica: en proyectos basados en Bundler con Ruby 3.4 o posterior, lista
gem "base64"en tu Gemfile (o instálala congem install base64). - 2025, Ruby 4.0: llega la versión 0.3.0, que añade firmas de tipos RBS, entre otro mantenimiento.
A través de todo esto, un hecho nunca cambió: el módulo son unas pocas docenas de líneas de Ruby puro encima de las plantillas pack y unpack del core. Sin extensión C, sin dependencias, sin nada que compilar - y un contador de descargas en los cientos de millones en rubygems.org.
Curiosidades de Ruby para los curiosos
- El lado de decodificación del módulo son dos cuerpos de método de una línea,
str.unpack1("m")ystr.unpack1("m0"), más la variante urlsafe, que es un intercambio de letras y una corrección de padding encima de la estricta. Puedes borrar el require y escribirlo tú mismo. - El propio
Net::HTTPde Ruby ni siquiera usa el móduloBase64para el Basic auth - llama a la plantillapackdirectamente:["user:pass"].pack("m0"). - Las cookies firmadas y cifradas de Rails son cadenas Base64 por dentro: el codificador de mensajes de ActiveSupport elige
strict_encode64para cookies normales yurlsafe_encode64conpadding: falsepara IDs firmados URL-safe. Probablemente hayas decodificado una sin saberlo. - Cada clase de digest tiene un método
base64digest-Digest::SHA256.base64digest("hello")- un one-liner para checksums que tienen que vivir en texto. - La etiqueta
!binaryde YAML es Base64. Vuelca una cadena BINARY con Psych y el formato hace el encoding por ti en silencio. decode64no le importa si tus líneas son de 60, 64 o 76 caracteres, o una sola línea gigante. La plantillamsalta los saltos de línea, así que las entradas dobladas y las sin doblar se decodifican idénticas.
Sigue adelante
Ahora tienes el kit de decodificación completo: un lector indulgente para los blobs con forma MIME, un guardián estricto para todo lo que no sea de confianza, un diplomático URL-safe para tokens y enlaces, y el paso del encoding que convierte los bytes resultantes en texto que Ruby te deje usar. La dirección inversa - decidir a cuál de los tres codificadores de Ruby alimentar con tus bytes, y controlar el alfabeto, el padding y los saltos de línea - trae su propio lote de sorpresas, empezando por un salto de línea final que nadie pidió. Ese lado de la calle se cubre a fondo en el artículo de codificación Base64, enlazado más abajo.
Última actualización: 2026-09-08
Artículo relacionado: Codificación Base64 en Ruby: una guía completa