Decodificação Base64 em Ruby: um guia completo
Em algum lugar entre você e os dados originais existe uma muralha de caracteres: letras maiúsculas e minúsculas, dígitos, talvez um mais, uma barra ou um hífen, e quem sabe um sinal de igual estacionado no final. Seu editor não faz a menor ideia do tipo de arquivo que aquilo é. Seu banco de dados enfiou isso numa coluna de texto. A coisa chegou num cabeçalho HTTP, numa URL, numa chave YAML ou num chamado de suporte com um anexo .b64. Você reconhece na hora - Base64 - e agora precisa dos bytes de volta. Em Ruby, essa necessidade está a um statement require e a uma chamada de método de distância.
Caso o formato seja novo para você, aqui vai a versão de trinta segundos. O Base64 reescreve dados brutos três bytes por vez: cada grupo de três bytes vira quatro caracteres retirados de um alfabeto de 64 símbolos, e quando a entrada não divide por três certinho, um ou dois caracteres = são anexados como padding para que a saída sempre caia em um múltiplo de quatro. Decodificar é a viagem de volta - quatro caracteres entrando, três bytes saindo - então o resultado é sempre menor que a entrada, mais ou menos três quartos do tamanho. A página inicial deste site percorre o formato inteiro em detalhe, então este guia gasta sua energia onde ela pertence: no lado Ruby do trabalho.
A boa notícia: toda instalação do Ruby embarca o toolkit completo de decodificação. O módulo Base64 não precisa de nada instalado, e seus três decodificadores são tão pequenos que dá para ler todo o source deles numa sentada só. O aviso: o decodificador para o qual você vai primeiro é também o que nunca reclama, o que é uma propriedade linda para e-mail e uma propriedade terrível para segurança. No final deste guia você vai saber exatamente o que cada decodificador aceita, como transformar os bytes que ele devolve em texto que o Ruby vai deixar você usar, e o que fazer com cada payload que um desenvolvedor Ruby de verdade decodifica - JWTs, cabeçalhos de autenticação, data URIs, corpos de e-mail, armadura PEM, arquivos, blobs de config e os enormes.
Conheça a caixa de ferramentas
Tudo começa com um require. Não há etapa de instalação, nem peculiaridades de plataforma, nem extensão nativa para compilar:
require "base64"
puts Base64::VERSION
# => 0.2.0 num Ruby 3.3 padrão, por exemplo
Aqui está o lado inteiro de decodificação do toolkit em uma única tabela, na ordem da frequência com que você vai recorrer a cada método:
| Decodificador | Tratamento de caracteres estrangeiros | Regras de padding | Quando algo dá errado |
|---|---|---|---|
Base64.decode64(str) |
ignora tudo que não está no alfabeto padrão, incluindo quebras de linha e espaços | qualquer coisa, até padding errado | nada - nunca levanta, simplesmente devolve o que conseguiu decodificar |
Base64.strict_decode64(str) |
rejeita qualquer caractere fora do alfabeto padrão | precisa estar presente e exatamente correto | levanta ArgumentError |
Base64.urlsafe_decode64(str) |
aceita o alfabeto URL-safe e o padrão, rejeita o resto | opcional, mas se presente precisa estar correto | levanta ArgumentError |
Se você gosta de saber o que suas ferramentas fazem por baixo do capô, o lado inteiro de decodificação do módulo é um wrapper fino em volta de dois templates do mecanismo core pack/unpack, implementado em C dentro do core do Ruby:
# o lado inteiro de decodificação do módulo, condensado
def decode64(str)
str.unpack1("m")
end
def strict_decode64(str)
str.unpack1("m0")
end
O template m é o leitor tolerante, m0 é o estrito, e essa diferença de um único caractere explica todo o abismo de personalidade entre os dois primeiros decodificadores. Como o trabalho pesado acontece na velocidade do core, o módulo continua Ruby puro enquanto mastiga megabytes em menos de dez milissegundos.
decode64: o camaleão
Base64.decode64 é o decodificador que diz sim para tudo. Dê a ele um payload limpo e ele decodifica. Dê a ele um blob estilo MIME cheio de quebras de linha e ele dá de ombros. Dê a ele uma string que nem é Base64 e ele devolve o que conseguiu espremer, sem um único aviso:
require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"
Essa segunda linha é toda a personalidade em um exemplo só. O decodificador pula tudo que não faz parte do alfabeto padrão - quebras de linha, espaços, o caractere de controle esporádico - e decodifica o resto. Esse é exatamente o comportamento que o Base64 MIME deveria ter, e é por isso que decode64 é a ferramenta certa para tudo que viajou por e-mail.
O outro lado é o que o torna perigoso. Como o decodificador nunca reclama, ele também nunca te avisa quando a entrada estava errada:
Base64.decode64("not base64 at all!")
# => dez bytes de lixo com aparência perfeitamente plausível
Base64.decode64("====")
# => ""
O primeiro exemplo acha os caracteres que por acaso são letras válidas do alfabeto, os decodifica, e devolve bytes nos quais você pode se sentir tentado a escrever direto num arquivo. O segundo exemplo devolve uma string vazia para uma string de quatro caracteres de padding. Nada é levantado, nada é registrado. Se a sua entrada não é confiável, esse silêncio é um recurso que você quer desligar - e é para isso que existem os dois próximos decodificadores.
Uma peculiaridade a mais que vale conhecer, porque é o tipo de coisa que se esconde na produção por meses: a decodificação para no primeiro caractere =. Qualquer coisa depois do padding não é erro; simplesmente nunca é lida:
Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello" a parte "Zm9vYmFy" é invisível para o decodificador
strict_decode64: o porteiro
Base64.strict_decode64 é o decodificador com prancheta. Ele aceita apenas o alfabeto padrão (A até Z, a até z, 0 até 9, mais, barra), exige que qualquer padding esteja exatamente certo e se recusa a produzir um único byte se alguma regra for quebrada:
Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => levanta ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => levanta ArgumentError
A última linha é a que explica tudo: o mesmo payload que decode64 decodificou com prazer agora levanta por causa de uma única quebra de linha. Padding faltando, padding a mais, um hífen, um underscore, um espaço - tudo isso é crime, e o payload inteiro afunda junto:
begin
Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
puts e.message
end
# => invalid base64
O porteiro até fiscaliza cantos do formato que você não pensaria em checar. Quando uma string Base64 termina com padding, alguns dos bits do último caractere nunca são usados, e a RFC diz que um codificador conformante precisa zerar esses bits. O Ruby verifica:
Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => levanta ArgumentError (os bits de padding não são zero)
A segunda string decodificaria para o mesmo byte da primeira se o decodificador fosse descuidado. O Ruby não é descuidado. Na prática, isso torna strict_decode64 o padrão certo para qualquer entrada que você não codificou: ele transforma erros de digitação, truncamento e alfabeto errado em erros altos e capturáveis, em vez de corrupção silenciosa.
urlsafe_decode64: o diplomata
Base64.urlsafe_decode64 existe para payloads que viajam onde + e / são palavras reservadas: URLs, tokens, identificadores de banco de dados. Internamente, ele traduz o alfabeto URL-safe (hífen e underscore) de volta para o padrão, normaliza o padding e entrega o resultado ao decodificador estrito:
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => levanta ArgumentError (quinze caracteres pedem um caractere de padding, não dois)
O primeiro exemplo mostra seu traço mais útil: entrada sem padding é válida. Se a string não tem padding e seu comprimento não é múltiplo de quatro, o decodificador acrescenta os caracteres = que faltam por você - que é exatamente o que os JSON Web Tokens, o maior consumidor de Base64 URL-safe, produzem. Se o padding está presente, porém, ele precisa estar correto, igual ao decodificador estrito.
Há uma peculiaridade que a documentação não grita: o diplomata fala os dois idiomas. Como o método reescreve hífens e underscores antes de fazer um decode estrito, ele também aceita strings do alfabeto padrão:
Base64.urlsafe_decode64("aGVsbG8=")
# => "hello" o alfabeto padrão também é aceito
Essa tolerância é conveniente, mas significa que você não pode usar este método para descobrir de qual alfabeto veio um payload. Se isso importa para você, inspecione os caracteres você mesmo antes de decodificar.
E, diferente do decode64, o diplomata não tem misericórdia com espaços em branco. Uma quebra de linha em qualquer lugar de um payload URL-safe levanta ArgumentError, então se a sua entrada vem de um arquivo com linhas quebradas, remova as quebras de linha primeiro.
Bytes não são texto: a etapa de encoding
Aqui está a etapa que derruba até desenvolvedores experientes, porque o Ruby a torna visível. Uma string Base64 decodificada é sempre marcada com o encoding ASCII-8BIT (também chamado de BINARY), não importa se os dados originais eram um PNG, um payload de JWT ou uma carta de amor em 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]
Se o payload é binário - uma imagem, um arquivo zip, um hash - você mantém exatamente assim e escreve com File.binwrite. Sem conversão, sem perguntas. Se o payload é texto, os bytes são quase com certeza UTF-8, e você precisa dizer isso ao 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
As duas chamadas fazem trabalhos diferentes. force_encoding só troca a etiqueta dos bytes; valid_encoding? então verifica se eles formam UTF-8 de verdade. Rode nessa ordem, porque validar uma string BINARY primeiro não tem nada para validar. E uma pequena armadilha de comparação para lembrar a vida inteira: o Ruby só considera uma string BINARY igual a uma string UTF-8 quando as duas são ASCII puro, então troque a etiqueta antes de comparar texto decodificado com o seu original:
decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false mesmos bytes, etiquetas diferentes
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true
JWTs: lendo um token sem a chave
Um JSON Web Token é três strings Base64 grampeadas com pontos: header, payload, assinatura. As duas primeiras são o Base64 URL-safe, sem padding, de documentos JSON, o que significa que um token é legível por qualquer pessoa que o veja - incluindo você, sem nenhuma biblioteca:
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 trabalho de verdade você vai usar a gem jwt, que cuida da parte que de fato te protege - a assinatura - e das validações de claims:
# no 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
Duas notas de segurança cabem aqui, porque as duas já custaram incidentes de verdade. Primeira: o payload não é cifrado; decodificá-lo é ler, não violar, e a assinatura é a única proteção, então nunca trate um payload decodificado como entrada confiável. Segunda: fixe o algoritmo no JWT.decode exatamente como mostrado. Omitir deixa o próprio header do token decidir como ele é verificado, e é esse pedacinho de flexibilidade que os famosos ataques de confusão de algoritmo de JWT exploram.
Basic Auth: a senha escondida à vista de todos
O cabeçalho de autenticação mais antigo da web é o próprio Base64. A autenticação Basic do HTTP envia as credenciais como user:password, codificadas, depois da palavra Basic - e o cabeçalho viaja junto em todo request, então ele aparece em todo log que você vai debugar. Decodificar um é um trabalho de remover e 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!
O limite de 2 no split importa: uma senha pode, legalmente, conter dois-pontos, e você só quer cortar no primeiro. A própria biblioteca padrão do Ruby monta esse cabeçalho no sentido inverso, no Net::HTTP, usando o template core pack diretamente:
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
E a nota de segurança que precisa ser dita mesmo sendo óbvia: Base64 é um tradutor, não uma fechadura. Basic auth só é aceitável sobre HTTPS. O encoding existe para que credenciais possam viajar pelo cabo como texto imprimível, não para que sejam secretas.
Data URIs: a imagem que não é um arquivo
Um data URI esconde um arquivo inteiro dentro de uma URL: um media type, a palavra base64, uma vírgula e os bytes codificados. Navegadores os renderizam em tags img e em CSS, e apps HTML de arquivo único os adoram porque não há segundo request para fazer. Montar um no Ruby leva uma linha:
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
Decodificar um é o caminho inverso, com dois detalhes que derrubam as pessoas. A vírgula é o separador, então divida exatamente uma vez, e a parte do media type pode ser qualquer coisa, 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)
Use strict_decode64 aqui, não decode64: um payload de data URI é uma linha limpa só, e você quer um erro alto se ele estiver corrompido. Também fique de olho no imposto de tamanho - toda imagem que você embute cresce por volta de um terço - então data URIs são perfeitos para favicons, logos pequenos e fontes, e uma péssima ideia para fotos de destaque.
E-mail: linhas de 60 caracteres e a gem mail
Base64 foi inventado para e-mail, e as cicatrizes aparecem. SMTP foi desenhado para linhas curtas de texto de sete bits, então o Base64 MIME embrulha sua saída em linhas curtas, e um decodificador conformante precisa ignorar as quebras de linha. O decode64 do Ruby se comporta exatamente assim, então um corpo MIME embrulhado é comida fácil:
body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"
Você raramente vai escrever isso na mão. A gem mail faz o trabalho MIME inteiro por você: anexos são codificados em Base64 automaticamente, as linhas são quebradas a cada 60 caracteres, com folga dentro do limite de 76 caracteres do MIME, e os cabeçalhos corretos são anexados:
# no 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
# a parte do anexo carrega Content-Transfer-Encoding: base64
O mesmo truque se esconde dentro de cabeçalhos de e-mail. Uma linha de assunto não-ASCII chega como uma palavra codificada RFC 2047: um charset, a letra B e Base64 entre pontos de interrogação. Decodificar uma na mão é um pequeno exercício de cirurgia em string:
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: chaves e certificados em armadura
Chaves e certificados passam a maior parte da vida dentro de armadura PEM: uma linha BEGIN, um bloco de Base64 e uma linha END. A armadura vem dos anos 80 - Privacy-Enhanced Mail é onde a linhagem inteira do Base64 começa - mas ainda é o formato que seus arquivos .crt e .key vestem hoje.
Decodificar um arquivo PEM na mão é só tirar a armadura e deixar o decodificador tolerante mastigar as quebras de linha:
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 de verdade você normalmente pula a etapa manual e entrega a string PEM inteira ao OpenSSL, que lê a armadura ele mesmo:
require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, ou o que a chave for no fim
O único detalhe de interoperabilidade que vale saber: linhas PEM classicamente têm 64 caracteres, e o decodificador ignora quebras de linha de qualquer jeito, então um embrulho de 60 caracteres ou uma linha gigante decodificam tão bem quanto.
Arquivos e a convenção .b64
O formato de arquivo mais comum do mundo Base64 é um arquivo de texto puro com extensão .b64 (ou às vezes .base64) guardando um payload codificado. Ler um é uma viagem de ida e volta em três passos:
require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)
Use File.binwrite na saída - um PNG ou zip decodificado é binário, e escrita em modo texto o corromperia em plataformas que traduzem finais de linha. Se o seu arquivo .b64 veio de uma ferramenta que quebra linhas, o decode64 trata as quebras de linha de graça. Se você quer validar em vez de tolerar, leia o arquivo em modo binário e remova as quebras de linha antes de um decode estrito:
encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)
A leitura binária importa no Windows, onde o modo texto reescreve finais de linha CRLF como LF - exatamente o tipo de mutação que você não quer acontecendo dentro de uma string que você está prestes a validar.
Base64 URL-safe: payloads que viajam em links
Esse é o ponto de vista do decodificador sobre a variante URL-safe, porque a escolha que você faz aqui muda qual dos três decodificadores você vai usar. Base64 URL-safe (RFC 4648, seção 5) troca os dois caracteres que as URLs não gostam - + vira -, / vira _ - e geralmente joga o padding fora também. No Ruby você vai encontrá-lo em parâmetros de query, valores de cookie, identificadores de API, IDs de vídeo estilo YouTube e, claro, JWTs.
Aqui está como os três decodificadores se comportam nas mesmas entradas, porque as diferenças são exatamente onde os bugs nascem:
| Entrada | decode64 | strict_decode64 | urlsafe_decode64 |
|---|---|---|---|
aGVsbG8= (padrão, com padding) |
"hello" |
"hello" |
"hello" |
aGVsbG8 (sem padding) |
"hello" |
ArgumentError |
"hello" |
SGVsbG8gd29ybGQ- (hífen no último grupo) |
"Hello world" (um byte a menos!) |
ArgumentError |
12 bytes, a resposta correta |
aGVsbG8=\n (quebra de linha no final) |
"hello" |
ArgumentError |
ArgumentError |
aGVs!bG8= (ponto de exclamação perdido) |
"hello" |
ArgumentError |
ArgumentError |
A terceira linha é a que morde as pessoas. Um payload URL-safe decodificado com o decodificador padrão perde em silêncio o último byte em vez de levantar qualquer coisa, porque decode64 simplesmente ignora o hífen. Se um payload pode vir de uma URL, decodifique com urlsafe_decode64.
Uma nota prática: se um dia você precisar levar um payload URL-safe para um contexto que só entende o alfabeto padrão (uma biblioteca, um sistema de fora), o truque clássico de interoperabilidade - traduzir o alfabeto e acrescentar o padding você mesmo - são três linhas:
def standardize_urlsafe(b64)
b64 = b64.tr("-_", "+/")
b64 += "=" * ((4 - b64.length % 4) % 4)
b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"
Você raramente vai precisar - urlsafe_decode64 já adiciona padding por você - mas é o padrão a reconhecer no código dos outros, e o padrão a usar quando o alfabeto padrão é o que o outro lado espera.
Config, variáveis de ambiente e bancos de dados
Base64 aparece em configuração sempre que dados binários precisam morar dentro de um documento de texto. Um arquivo .env, uma config YAML ou um blob de configurações JSON não conseguem carregar bytes crus com segurança, então os bytes são codificados, e algo no seu aplicativo precisa decodificá-los na inicialização:
require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)
YAML ganha uma menção especial, porque o formato tem uma tag binária nativa. Quando você faz dump de uma string BINARY, o Psych a escreve como um scalar !binary guardando Base64, e carregá-la devolve seus bytes intactos - sem encoding manual algum:
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
Em bancos de dados a regra prática é: se o seu banco tem um tipo binário de verdade, use. Base64-em-uma-coluna-TEXT é o padrão para quando a camada de armazenamento só fala strings - alguns document stores, APIs em formato JSON, ou um schema legado que você não pode mudar - e o preço é o imposto de um terço no tamanho da coluna, além da disciplina de decodificar na entrada e re-codificar na saída em cada fronteira.
Entradas grandes, memória estável
O módulo é baseado em buffer: uma chamada de decode lê a string inteira de uma vez e devolve o resultado inteiro. Não existe decodificador streaming na biblioteca padrão, então o conselho honesto para payloads grandes é planejar a memória. A boa notícia é que decodificar só deixa as coisas menores - a saída é no máximo três quartos da entrada - então a string de entrada é a sua única alocação grande.
Se um payload é grande o suficiente para te preocupar, você pode decodificá-lo em grupos de quatro caracteres, porque grupos de quatro do Base64 são autocontidos e o grupo final parcial carrega o próprio 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
Isso funciona em entrada limpa e sem quebras de linha - as mesmas regras que strict_decode64 impõe - porque um grupo final solto só é válido com o padding presente. Para arquivos realmente enormes, arquivos de vários gigabytes e afins, o padrão é ler o arquivo em fatias, decodificar cada fatia e fazer stream dos bytes para o disco, para que só uma fatia esteja na memória de cada vez.
One-liners para o terminal
Você não precisa de um arquivo de script para decodificar coisas no shell. O Ruby pode fazer require do módulo na hora:
ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world
E para arquivos, passe um caminho de arquivo em vez do próprio payload:
ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin
Dois perigos moram aqui. Primeiro, se você passar por um pipe com echo ou qualquer comando de texto, uma quebra de linha final viaja junto, e o strict_decode64 vai levantar nela - use decode64, ou chomp a entrada:
echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'
Segundo, mantenha print em vez de puts para saída binária, porque puts acrescenta uma quebra de linha dele e corromperia o final do seu arquivo restaurado.
Armadilhas que desenvolvedores Ruby realmente pegam
- decode64 nunca levanta. Lixo entra, lixo sai. Se a sua entrada não é confiável e você aceita em silêncio bytes corrompidos, o bug vai aparecer semanas depois num arquivo corrompido, não na linha do decode. Padrão: decodificador estrito para tudo que você não codificou.
- strict_decode64 e a quebra de linha final. Arquivos de texto, pipes de echo e copy-paste adoram terminar com um newline, e o decodificador estrito levanta
ArgumentErrornela.chompa entrada primeiro - ou leia em modo binário e remova as quebras de linha. - Esquecer a etapa de encoding. Uma string decodificada é BINARY até você dizer o contrário. Force UTF-8 (e confira a validade) antes de tratar o resultado como texto, ou você vai ter mojibake e
Encoding::CompatibilityErrorno momento em que misturá-lo com strings UTF-8. - Comparar BINARY com UTF-8. Mesmos bytes, etiquetas diferentes, e
==diz false - a menos que a string seja por acaso ASCII puro. Troque a etiqueta antes de comparar. - Entrada URL-safe pelo decodificador errado. Hífens e underscores são descartados em silêncio pelo
decode64, então um payload URL-safe volta um byte a menos e corrompido, sem nenhum erro. Useurlsafe_decode64. - Dados depois do padding são invisíveis.
decode64para no primeiro=. Ótimo para MIME, terrível para pegar um payload que foi truncado e depois re-padding por outra ferramenta. - Padding não canônico é aceito em silêncio. Uma string como
QR==carrega bits de padding que um codificador adequado teria zerado;decode64decodifica com prazer enquantostrict_decode64rejeita. Nada vai te dizer nunca que o seu codificador estava mentindo. - Leituras de arquivo em modo texto no Windows reescrevem finais de linha antes de você ver. Leia arquivos
.b64em modo binário quando a intenção for validá-los.
Bons hábitos para o lado do decode
- Escolha o decodificador pela origem dos dados:
strict_decode64para tudo que não é confiável (e rescueArgumentErrorcomo sua ramificação de entrada inválida),urlsafe_decode64para payloads nascidos em URL,decode64só para formatos genuinamente tolerantes, como corpos MIME. - No momento em que os bytes são decodificados, decida a identidade deles: binário (mantém ASCII-8BIT, escreve com
File.binwrite) ou texto (force_encodingpara UTF-8, entãovalid_encoding?antes de usar). - Não decodifique e confie. Um payload de JWT é legível justamente porque é Base64; a assinatura decide se ele é de verdade. Uma string Base64 num arquivo de config é dado, não prova.
- Quando escrever validadores, teste-os nos casos chatos: a string vazia, entrada sem padding, entrada quebrada em linhas, entrada URL-safe e padding errado. São esses os casos que separam os três decodificadores.
Uma breve história do Base64 no Ruby
O módulo Base64 faz parte da biblioteca padrão do Ruby há mais de quinze anos, e o jeito como ele é distribuído mudou mais do que você imagina:
- 2008, Ruby 1.8.7: o módulo chega com
encode64,decode64, mais dois métodos que não existem mais -b64encode(quebra de linha num comprimento escolhido) edecode_b(decodificação de cabeçalhos de e-mail RFC 2047). Livros antigos e até algumas gems antigas ainda os citam, e chamar qualquer um deles hoje é umNoMethodError. - 2009, a linha 1.9:
strict_encode64,strict_decode64,urlsafe_encode64eurlsafe_decode64chegam, e os dois métodos legacy são aposentados (o 1.9.1 já tinha lançado as duas mudanças em janeiro de 2009). - 2015, Ruby 2.3:
urlsafe_encode64ganha a keywordpadding:, permitindo saída sem padding para tokens e URLs. - 2020, Ruby 3.0: base64 é extraído da biblioteca padrão para sua própria gem, versão 0.1.0, no repositório
ruby/base64. Ele chega como default gem, entãorequire "base64"continua funcionando sem cerimônia. - 2023, Ruby 3.3: a versão 0.2.0 adiciona
Base64::VERSIONe um conjunto de documentação bem mais rico. - 2024, Ruby 3.4: a gem é reclassificada de default gem para bundled gem. A consequência prática: em projetos baseados em Bundler com Ruby 3.4 ou posterior, liste
gem "base64"no seu Gemfile (ou instale comgem install base64). - 2025, Ruby 4.0: a versão 0.3.0 chega, adicionando assinaturas de tipo RBS entre outras manutenções.
Através de tudo isso, um fato nunca mudou: o módulo é de algumas dúzias de linhas de Ruby puro sentadas em cima dos templates core pack e unpack. Sem extensão em C, sem dependências, nada para compilar - e uma contagem de downloads na casa das centenas de milhões no rubygems.org.
Curiosidades Ruby para os curiosos
- O lado de decodificação do módulo tem dois corpos de método de uma linha,
str.unpack1("m")estr.unpack1("m0"), mais a variante urlsafe, que é uma troca de letras e um ajuste de padding por cima da estrita. Dá para apagar o require e escrever você mesmo. - O próprio
Net::HTTPdo Ruby nem usa o móduloBase64para Basic auth - ele chama o templatepackdiretamente:["user:pass"].pack("m0"). - Os cookies assinados e cifrados do Rails são strings Base64 por baixo do capô: o message codec do ActiveSupport escolhe
strict_encode64para cookies normais eurlsafe_encode64compadding: falsepara IDs assinados URL-safe. Você provavelmente já decodificou um sem saber. - Toda classe de digest tem um método
base64digest-Digest::SHA256.base64digest("hello")- um one-liner para checksums que precisam morar em texto. - A tag
!binarydo YAML é Base64. Faça dump de uma string BINARY com Psych e o formato faz o encoding em silêncio por você. decode64não se importa se suas linhas têm 60, 64 ou 76 caracteres, ou uma linha gigante. O templatempula as quebras de linha, então entrada quebrada e sem quebra decodificam de forma idêntica.
Seguindo em frente
Com isso você tem o toolkit completo de decodificação: um leitor tolerante para blobs em formato MIME, um porteiro estrito para tudo que não é confiável, um diplomata URL-safe para tokens e links, e a etapa de encoding que transforma os bytes devolvidos em texto que o Ruby vai deixar você usar. A direção inversa - decidir qual dos três codificadores do Ruby alimentar com seus bytes, e controlar o alfabeto, o padding e as quebras de linha - tem o seu próprio conjunto de surpresas, começando por um newline final que ninguém pediu. Esse lado da rua é coberto em profundidade no artigo de codificação Base64, linkado abaixo.
Última atualização: 2026-09-08
Artigo relacionado: Codificação Base64 em Ruby: um guia completo