Subir un archivo
  1. base64hub
  2. Base64 en código
  3. Ruby
Código

Base64 en Ruby

El módulo Base64 de Ruby es una capa fina sobre Array#pack y String#unpack1. La trampa principal es que los métodos no estrictos añaden saltos de línea. Las salidas de esta página son de Ruby 3.4.

Codificar y decodificar una cadena

Usa los métodos strict_ salvo que quieras expresamente una salida MIME:

require "base64"

enc = Base64.strict_encode64("héllo wörld")
puts enc          # aMOpbGxvIHfDtnJsZA==

dec = Base64.strict_decode64(enc).force_encoding("UTF-8")
puts dec          # héllo wörld

Las cadenas de Ruby llevan asociada una codificación, y las cadenas decodificadas siempre vuelven como BINARY (ASCII-8BIT), porque Base64 no sabe qué significan los bytes. force_encoding("UTF-8") les cambia la etiqueta sin copiarlas. Si te lo saltas, las comparaciones con literales UTF-8 fallan y los métodos de cadena tratan cada byte como un carácter.

Si la cadena de origen está en otra codificación (por ejemplo, Windows-1252 de un archivo antiguo), llama a .encode("UTF-8") antes de codificar a Base64, o el receptor verá bytes distintos de los que esperas.

encode64 frente a strict_encode64

encode64 es la variante MIME. Inserta un salto de línea cada 60 caracteres y siempre termina con uno:

Base64.encode64("hi")          # => "aGk=\n"
Base64.encode64("x" * 60)
# => "eHh4...eHh4\neHh4eHh4eHh4eHh4eHh4\n"   (60 chars per line)
Base64.strict_encode64("hi")   # => "aGk="

Ese \n final es el error de Base64 más habitual en Ruby: rompe cabeceras HTTP, firmas y comparaciones de JSON. Fíjate en que Ruby corta a 60 caracteres, mientras que el RFC 2045 permite hasta 76 y la mayoría de los demás lenguajes usan 76. Las dos longitudes son válidas para el correo; si necesitas exactamente 76, el codificador Base64 MIME muestra ese formato.

Base64 URL-safe con y sin relleno

Base64.urlsafe_encode64("<<???>>")                  # => "PDw_Pz8-Pg=="
Base64.urlsafe_encode64("<<???>>", padding: false)  # => "PDw_Pz8-Pg"
Base64.urlsafe_decode64("PDw_Pz8-Pg")               # => "<<???>>"

urlsafe_decode64 acepta entradas con o sin relleno: añade los = que faltan y después decodifica en modo estricto. La palabra clave padding: existe desde Ruby 2.3. Para JWT y otros tokens, consulta Base64 frente a Base64URL o prueba el decodificador Base64URL.

Leer los claims de un JWT, sin verificarlo:

require "json"

jwt = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0In0.sig"
header, payload, _sig = jwt.split(".")
JSON.parse(Base64.urlsafe_decode64(header))    # => {"alg" => "HS256"}
JSON.parse(Base64.urlsafe_decode64(payload))   # => {"sub" => "1234"}

El formato de salida {"alg" => "HS256"} es el nuevo Hash#inspect de Ruby 3.4; las versiones anteriores imprimen {"alg"=>"HS256"}.

Codificar y decodificar un archivo

Lee y escribe en modo binario; si no, la traducción de saltos de línea de Windows y la conversión de codificación pueden cambiar los bytes:

puts Base64.strict_encode64(File.binread("hello.txt"))   # aGVsbG8K
File.binwrite("copy.txt", Base64.strict_decode64("aGVsbG8K"))

Archivos grandes por bloques

El módulo Base64 no tiene una API de flujo, pero pack("m0") por bloques funciona siempre que cada bloque sea múltiplo de 3 bytes:

File.open("big.bin", "rb") do |src|
  File.open("big.b64", "wb") do |dst|
    while (chunk = src.read(3 * 1024 * 64))   # 192 KiB, divisible by 3
      dst.write([chunk].pack("m0"))
    end
  end
end

Con un archivo de prueba de 300 KB, el resultado fue idéntico al de strict_encode64(File.binread(...)). Con cualquier otro tamaño de bloque aparece relleno = en mitad de la salida.

pack y unpack1 directamente

El módulo es una capa de comodidad. Esto es equivalente y no necesita require:

["hi"].pack("m0")    # => "aGk="    (same as strict_encode64)
["hi"].pack("m")     # => "aGk=\n"  (same as encode64)
"aGk=".unpack1("m0") # => "hi"      (same as strict_decode64)

Validación y errores

strict_decode64 lanza ArgumentError con el mensaje invalid base64 ante cualquier cosa que no sea Base64 canónico con relleno. Probamos seis entradas y todas lo lanzaron:

EntradaProblema
PDw_Pz8-Pg==caracteres URL-safe
aGkfalta el relleno
aGk=!!basura después del relleno
alongitud imposible
aGl=bits de relleno distintos de cero
"aGVs\nbG8K"salto de línea
def base64?(s)
  Base64.strict_decode64(s)
  true
rescue ArgumentError
  false
end

El decode64 no estricto nunca lanza errores. Se salta todo lo que no entiende, lo que produce basura en lugar de un error:

Base64.decode64("aGk=!!")        # => "hi"
Base64.decode64("PDw_Pz8-Pg==")  # => "<<\x0F\xCF\xC3\xE0"   (should be "<<???>>")

Usa decode64 solo con cuerpos MIME que tengan saltos de línea, y solo si confías en la fuente. Cuando una cadena se niega a decodificarse, el validador Base64 señala el problema y errores habituales de Base64 explica las causas habituales.

Ruby 3.4 y Bundler

Ruby 3.4 pasó base64 de las gemas por defecto a las gemas incluidas (bundled). Fuera de Bundler no cambia nada. Con bundle exec, un Gemfile que no la incluya falla:

warning: base64 was loaded from the standard library, but is not part of the
default gems starting from Ruby 3.4.0.
...
cannot load such file -- base64 (LoadError)

Añade gem "base64" al Gemfile o al gemspec, o sustituye las llamadas por pack("m0") / unpack1("m0"), que forman parte del núcleo de Ruby.

Preguntas frecuentes

¿Por qué Base64.encode64 añade \n a mi cadena?

encode64 genera una salida RFC 2045 con un salto de línea cada 60 caracteres y otro al final. Usa strict_encode64 para cabeceras, JSON y URL; nunca añade saltos de línea.

¿Por qué obtengo «cannot load such file -- base64» en Ruby 3.4?

Desde Ruby 3.4, base64 es una gema incluida (bundled) y no una gema por defecto. El ruby normal la sigue cargando, pero con Bundler tienes que añadir gem "base64" al Gemfile. O sáltate el require y usa [data].pack("m0").

¿Cómo decodifico Base64 sin relleno en Ruby?

Base64.urlsafe_decode64 añade por sí mismo el relleno que falta. Para el alfabeto estándar, añade "=" * (-s.length % 4) antes de llamar a strict_decode64.

Sigue leyendo