Décodage Base64 en Ruby : un guide complet
Quelque part entre vous et les données d'origine se dresse un mur de caractères : lettres en majuscules et en minuscules, chiffres, peut-être un plus, un slash ou un tiret, et sans doute un signe égal garé à la fin. Votre éditeur n'a aucune idée du type de fichier que c'est. Votre base de données l'a calé dans une colonne texte. Il est arrivé dans un en-tête HTTP, une URL, une clé YAML ou un ticket de support avec une pièce jointe .b64. Vous le reconnaissez en un éclair - Base64 - et maintenant il vous faut les octets. En Ruby, ce besoin tient en un seul énoncé require et un seul appel de méthode.
Si le format vous est nouveau, voici la version trente secondes. Le Base64 réécrit les données brutes trois octets à la fois : chaque groupe de trois octets devient quatre caractères tirés d'un alphabet de 64 symboles, et quand l'entrée ne se divise pas exactement par trois, on ajoute un ou deux caractères = en padding pour que la sortie tombe toujours sur un multiple de quatre. Le décodage est le trajet inverse - quatre caractères en entrée, trois octets en sortie - le résultat est donc toujours plus petit que l'entrée, environ trois quarts de la taille. La page d'accueil de ce site détaille le format dans les moindres recoins, alors ce guide concentre son énergie là où elle doit aller : sur le côté Ruby du travail.
La bonne nouvelle : toute installation Ruby livre la boîte à outils de décodage complète. Le module Base64 n'exige rien d'installé, et ses trois décodeurs sont si petits que vous pouvez lire toute leur source d'une seule traite. La mise en garde : le décodeur vers lequel vous allez en premier est aussi celui qui ne se plaint jamais, une propriété magnifique pour l'e-mail et terrible pour la sécurité. À la fin de ce guide, vous saurez exactement ce que chaque décodeur accepte, comment transformer les octets qu'il rend en du texte que Ruby vous laissera utiliser, et quoi faire de chaque payload qu'un développeur Ruby décode vraiment - JWTs, en-têtes d'authentification, data URIs, corps d'e-mail, armure PEM, fichiers, blobs de configuration et des géants.
Faisons connaissance avec la boîte à outils
Tout commence par un require. Il n'y a pas d'étape d'installation, pas d'originalité de plateforme, pas d'extension native à compiler :
require "base64"
puts Base64::VERSION
# => 0.2.0 sur un Ruby 3.3 de base, par exemple
Voici toute la partie décodage de la boîte à outils dans un seul tableau, classé par fréquence d'utilisation de chaque méthode :
| Décodeur | Traitement des caractères étrangers | Règles de padding | Quand quelque chose cloche |
|---|---|---|---|
Base64.decode64(str) |
ignore tout ce qui n'est pas dans l'alphabet standard, y compris retours à la ligne et espaces | n'importe quoi, même un padding faux | rien - il ne lève jamais rien, il renvoie simplement ce qu'il a pu décoder |
Base64.strict_decode64(str) |
rejette tout caractère en dehors de l'alphabet standard | doit être présent et exactement correct | lève ArgumentError |
Base64.urlsafe_decode64(str) |
accepte l'alphabet sûr pour les URL et l'alphabet standard, rejette tout le reste | optionnel, mais s'il est présent il doit être correct | lève ArgumentError |
Si vous aimez savoir ce que vos outils font sous le capot, toute la partie décodage du module est un fin habillage autour de deux gabarits de la machinerie pack/unpack du core, elle-même implémentée en C dans le core Ruby :
# toute la partie décodage du module, condensée
def decode64(str)
str.unpack1("m")
end
def strict_decode64(str)
str.unpack1("m0")
end
Le gabarit m est le lecteur tolérant, m0 est le strict, et cette différence d'un seul caractère explique tout l'écart de personnalité entre les deux premiers décodeurs. Comme le gros du travail se fait à la vitesse du core, le module reste en Ruby pur tout en dévorant des mégaoctets en moins de dix millisecondes.
decode64 : le caméléon
Base64.decode64 est le décodeur qui dit oui à tout. Donnez-lui un payload propre et il le décode. Donnez-lui un blob à la MIME plein de retours à la ligne et il hausse les épaules. Donnez-lui une chaîne qui n'est pas du Base64 du tout et il rend ce qu'il a pu en tirer, sans le moindre avertissement :
require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"
Cette seconde ligne, c'est toute sa personnalité en un seul exemple. Le décodeur saute tout ce qui ne fait pas partie de l'alphabet standard - retours à la ligne, espaces, le caractère de contrôle qui traîne - et décode le reste. C'est exactement le comportement que le Base64 MIME est censé avoir, c'est pourquoi decode64 est l'outil juste pour tout ce qui a voyagé par e-mail.
L'envers du décor est ce qui le rend dangereux. Parce que le décodeur ne se plaint jamais, il ne vous dit jamais non plus quand l'entrée était fausse :
Base64.decode64("not base64 at all!")
# => dix octets de n'importe quoi qui a parfaitement l'air plausible
Base64.decode64("====")
# => ""
Le premier exemple repère les caractères qui se trouvent être des lettres valides de l'alphabet, les décode, et rend des octets que vous seriez tenté d'écrire tels quels dans un fichier. Le second exemple renvoie une chaîne vide pour une chaîne de quatre caractères de padding. Rien n'est levé, rien n'est journalisé. Si votre entrée n'est pas de confiance, ce silence est une fonctionnalité que vous voulez couper - et c'est exactement à ça que servent les deux décodeurs suivants.
Une dernière bizarrerie à connaître, parce que c'est le genre de chose qui se cache en production des mois durant : le décodage s'arrête au premier caractère =. Ce qui se trouve après le padding n'est pas une erreur ; il est simplement jamais lu :
Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello" la partie "Zm9vYmFy" est invisible pour le décodeur
strict_decode64 : le gardien
Base64.strict_decode64 est le décodeur avec sa feuille de contrôle. Il n'accepte que l'alphabet standard (de A à Z, de a à z, de 0 à 9, le plus, le slash), il exige que le padding soit exactement juste, et il refuse de produire le moindre octet si une seule règle est brisée :
Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => lève ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => lève ArgumentError
La dernière ligne est la plus révélatrice : le même payload que decode64 décodait joyeusement lève maintenant à cause d'un simple retour à la ligne. Padding manquant, padding en trop, un tiret, un tiret bas, un espace - tout ça est un crime, et tout le payload y passe :
begin
Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
puts e.message
end
# => invalid base64
Le gardien contrôle même des recoins du format que vous n'auriez pas pensé à vérifier. Quand une chaîne Base64 se termine par du padding, certains bits du dernier caractère ne sont jamais utilisés, et la RFC dit qu'un encodeur conforme doit mettre ces bits à zéro. Ruby vérifie :
Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => lève ArgumentError (les bits de padding ne sont pas à zéro)
La seconde chaîne se décoderait en le même octet que la première si le décodeur était laxiste. Ruby n'est pas laxiste. En pratique, ça fait de strict_decode64 le défaut juste pour toute entrée que vous n'avez pas encodée vous-même : il transforme les fautes de frappe, les troncatures et le mauvais alphabet en erreurs tonitruantes et capturables au lieu d'une corruption silencieuse.
urlsafe_decode64 : le diplomate
Base64.urlsafe_decode64 existe pour les payloads qui voyagent là où + et / sont des mots réservés : URLs, tokens, identifiants de base de données. En interne, il traduit l'alphabet sûr pour les URL (tiret et tiret bas) vers l'alphabet standard, normalise le padding, et passe le résultat au décodeur strict :
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => lève ArgumentError (quinze caractères demandent un caractère de padding, pas deux)
Le premier exemple montre son trait le plus utile : une entrée sans padding va bien. Si la chaîne n'a pas de padding et que sa longueur n'est pas un multiple de quatre, le décodeur ajoute les caractères = manquants à votre place - c'est exactement ce que produisent les JSON Web Tokens, le plus gros consommateur de Base64 sûr pour les URL. Mais si le padding est présent, il doit être correct, comme avec le décodeur strict.
Il y a une bizarrerie que la documentation ne crie pas sur les toits : le diplomate parle les deux langues. Parce que la méthode réécrit les tirets et les tirets bas avant de faire un décodage strict, elle accepte aussi les chaînes de l'alphabet standard :
Base64.urlsafe_decode64("aGVsbG8=")
# => "hello" l'alphabet standard est accepté aussi
Cette indulgence est pratique, mais ça veut dire que vous ne pouvez pas utiliser cette méthode pour savoir de quel alphabet vient un payload. Si c'est important pour vous, inspectez les caractères vous-même avant de décoder.
Et contrairement à decode64, le diplomate n'a aucune pitié pour les espaces blancs. Un retour à la ligne n'importe où dans un payload sûr pour les URL lève ArgumentError, donc si votre entrée vient d'un fichier enveloppé, retirez d'abord les retours à la ligne.
Les octets ne sont pas du texte : l'étape d'encodage
Voici l'étape qui fait trébucher même les développeurs expérimentés, parce que Ruby la rend visible. Une chaîne Base64 décodée est toujours étiquetée avec l'encodage ASCII-8BIT (alias BINARY), que les données d'origine soient un PNG, un payload JWT ou une lettre d'amour 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 le payload est binaire - une image, un fichier zip, un hash - vous le gardez exactement comme ça et vous l'écrivez avec File.binwrite. Pas de conversion, pas de questions. Si le payload est du texte, les octets sont presque certainement du UTF-8, et il faut le dire à 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
Les deux appels font des choses différentes. force_encoding rebadige juste les octets ; valid_encoding? vérifie ensuite qu'ils forment du vrai UTF-8. Exécutez-les dans cet ordre, parce que valider d'abord une chaîne BINARY n'a rien à valider. Et un petit piège de comparaison à retenir pour la vie : Ruby ne considère une chaîne BINARY égale à une chaîne UTF-8 que quand les deux sont en ASCII pur, alors rebadigez avant de comparer le texte décodé à votre original :
decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false mêmes octets, étiquettes différentes
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true
JWTs : lire un token sans la clé
Un JSON Web Token, c'est trois chaînes Base64 épinglées ensemble avec des points : en-tête, payload, signature. Les deux premières sont du Base64 sûr pour les URL, sans padding, de documents JSON, ce qui veut dire qu'un token est lisible par n'importe qui qui le voit - vous y compris, sans la moindre bibliothèque :
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"}
Pour du vrai travail vous utiliserez la gem jwt, qui gère la partie qui vous protège vraiment - la signature - et les validations des 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
Deux notes de sécurité méritent leur place ici, parce que les deux ont coûté de vrais incidents à de vraies personnes. Premièrement, le payload n'est pas chiffré ; le décoder, c'est lire, pas forcer, et la signature est la seule protection, alors ne traitez jamais un payload décodé comme une entrée de confiance. Deuxièmement, épinglez l'algorithme dans JWT.decode exactement comme montré. L'omettre laisse l'en-tête du token lui-même décider comment il est vérifié, et c'est précisément ce brin de flexibilité que les célèbres attaques de confusion d'algorithme JWT exploitent.
Basic auth : le mot de passe caché en plein jour
L'en-tête d'authentification le plus vieux du web, c'est le Base64 lui-même. L'authentification HTTP Basic envoie les identifiants sous la forme user:password, encodée, après le mot Basic - et l'en-tête accompagne chaque requête, donc il apparaît dans chaque log que vous déboguerez. En décoder un, c'est un travail de nettoyage et de découpe :
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!
La limite de 2 dans split a son importance : un mot de passe peut légitimement contenir des deux-points, et vous ne voulez jamais couper qu'au premier. La bibliothèque standard de Ruby elle-même construit cet en-tête en sens inverse, dans Net::HTTP, en utilisant le gabarit pack du core directement :
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
Et la note de sécurité qu'il faut dire même si elle est évidente : le Base64 est un traducteur, pas un cadenas. Le Basic auth n'est acceptable que sur HTTPS. L'encodage existe pour que les identifiants voyagent sur le fil en texte imprimable, pas pour qu'ils soient secrets.
Data URIs : l'image qui n'est pas un fichier
Une data URI cache un fichier entier dans une URL : un type média, le mot base64, une virgule, et les octets encodés. Les navigateurs les affichent dans des balises img et du CSS, et les applications HTML monofichier les adorent parce qu'il n'y a pas de seconde requête à faire. En construire une en Ruby tient en une ligne :
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
En décoder une, c'est l'opération inverse, avec deux détails qui font trébucher. La virgule est le séparateur, donc coupez exactement une fois, et la partie type média peut être n'importe quoi, y compris rien du tout :
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)
Utilisez strict_decode64 ici, pas decode64 : un payload de data URI est une seule ligne propre, et vous voulez une erreur tonitruante s'il est corrompu. Gardez aussi l'impôt de taille en tête - chaque image que vous intégrez grossit d'environ un tiers - donc les data URIs sont parfaites pour les favicons, les petits logos et les polices, et une mauvaise idée pour les photos hero.
E-mail : les lignes de soixante caractères et la gem mail
Le Base64 a été inventé pour l'e-mail, et on en voit encore les cicatrices. Le SMTP a été conçu pour de courtes lignes de texte à 7 bits, alors le Base64 MIME enveloppe sa sortie en courtes lignes, et un décodeur conforme doit ignorer les retours à la ligne. Le decode64 de Ruby se comporte exactement comme ça, donc un corps MIME enveloppé est une nourriture facile :
body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"
Vous l'écrirez rarement à la main. La gem mail fait tout le travail MIME pour vous : les pièces jointes sont encodées en Base64 automatiquement, les lignes sont enveloppées à 60 caractères, confortablement en dessous de la limite de 76 caractères du MIME, et les bons en-têtes sont attachés :
# 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 partie pièce jointe porte Content-Transfer-Encoding: base64
Le même tour se cache aussi dans les en-têtes d'e-mail. Une ligne d'objet non-ASCII arrive sous forme de mot encodé RFC 2047 : un jeu de caractères, la lettre B, et du Base64 entre points d'interrogation. En décoder un à la main, c'est un petit exercice de chirurgie sur chaînes :
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 : clés et certificats dans leur armure
Les clés et les certificats passent la plupart de leur vie dans une armure PEM : une ligne BEGIN, un bloc de Base64, et une ligne END. L'armure vient des années 80 - c'est la Privacy-Enhanced Mail d'où part toute la lignée du Base64 - mais c'est encore le format que portent vos fichiers .crt et .key aujourd'hui.
Décoder un fichier PEM à la main, c'est juste retirer l'armure et laisser le décodeur tolérant mâcher les retours à la ligne :
require "base64"
pem = File.read("server.key")
body = pem.lines
.reject { |line| line.start_with?("-----") || line.strip.empty? }
.join
key_bytes = Base64.decode64(body)
Pour un usage réel, vous sauterez habituellement l'étape manuelle et passerez la chaîne PEM entière à OpenSSL, qui lit l'armure lui-même :
require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, ou ce que la clé s'avère être
Le seul détail d'interopérabilité qui mérite d'être connu : les lignes PEM font classiquement 64 caractères, et le décodeur ignore les retours à la ligne dans tous les cas, donc qu'il s'agisse d'un enveloppage à 60 caractères ou d'une seule ligne géante, tout se décode aussi bien.
Fichiers et la convention .b64
Le format de fichier le plus courant du monde Base64 est un simple fichier texte avec une extension .b64 (ou parfois .base64) contenant un payload encodé. En lire un, c'est un aller-retour en trois étapes :
require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)
Utilisez File.binwrite à la sortie - un PNG ou un zip décodé est binaire, et une écriture en mode texte le corromprait sur les plateformes qui traduisent les fins de ligne. Si votre fichier .b64 vient d'un outil qui enveloppait les lignes, decode64 gère les retours à la ligne pour vous. Si vous voulez valider plutôt que tolérer, lisez le fichier en mode binaire et retirez les retours à la ligne avant un décodage strict :
encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)
La lecture binaire compte sur Windows, où le mode texte réécrit les fins de ligne CRLF en LF - exactement le genre de mutation que vous ne voulez pas voir arriver à l'intérieur d'une chaîne que vous êtes sur le point de valider.
Base64 sûr pour les URL : les payloads qui voyagent dans les liens
Voici la version décodeur de la variante sûre pour les URL, parce que le choix que vous faites ici change quel des trois décodeurs vous prenez. Le Base64 sûr pour les URL (RFC 4648, section 5) échange les deux caractères que les URLs n'aiment pas - + devient -, / devient _ - et supprime généralement le padding aussi. En Ruby vous le croiserez dans les paramètres de requête, les valeurs de cookies, les identifiants d'API, les IDs de vidéo façon YouTube, et bien sûr les JWTs.
Voici comment les trois décodeurs se comportent sur les mêmes entrées, parce que les différences sont exactement là où naissent les bugs :
| Entrée | decode64 | strict_decode64 | urlsafe_decode64 |
|---|---|---|---|
aGVsbG8= (standard, avec padding) |
"hello" |
"hello" |
"hello" |
aGVsbG8 (sans padding) |
"hello" |
ArgumentError |
"hello" |
SGVsbG8gd29ybGQ- (tiret dans le dernier groupe) |
"Hello world" (un octet en moins !) |
ArgumentError |
12 octets, la bonne réponse |
aGVsbG8=\n (retour à la ligne final) |
"hello" |
ArgumentError |
ArgumentError |
aGVs!bG8= (point d'exclamation errant) |
"hello" |
ArgumentError |
ArgumentError |
La ligne trois est celle qui mord. Un payload sûr pour les URL décodé avec le décodeur standard perd silencieusement son dernier octet au lieu de lever quoi que ce soit, parce que decode64 ignore simplement le tiret. Si un payload peut venir d'une URL, décodez-le avec urlsafe_decode64.
Une note pratique : si un jour vous devez déplacer un payload sûr pour les URL vers un contexte qui ne comprend que l'alphabet standard (une bibliothèque, un système extérieur), le tour d'interopérabilité classique - traduire l'alphabet et ajouter le padding vous-même - tient en trois lignes :
def standardize_urlsafe(b64)
b64 = b64.tr("-_", "+/")
b64 += "=" * ((4 - b64.length % 4) % 4)
b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"
Vous en aurez rarement besoin - urlsafe_decode64 ajoute déjà le padding pour vous - mais c'est le pattern à reconnaître dans le code des autres, et le pattern à prendre quand l'alphabet standard est ce que l'autre côté attend.
Configuration, variables d'environnement et bases de données
Le Base64 apparaît dans la configuration chaque fois que des données binaires doivent se trouver dans un document texte. Un fichier .env, une config YAML ou un blob de réglages JSON ne peut pas porter des octets bruts en toute sécurité, alors les octets sont encodés, et quelque chose dans votre application doit les décoder au démarrage :
require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)
Le YAML mérite une mention spéciale, parce que le format a un tag binaire natif. Quand vous dump une chaîne BINARY, Psych l'écrit comme un scalaire !binary contenant du Base64, et la relecture vous rend vos octets intacts - pas d'encodage manuel du tout :
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
Dans les bases de données, la règle du pouce est : si votre base de données a un vrai type binaire, utilisez-le. Le Base64-dans-une-colonne-TEXT est le pattern que vous prenez quand la couche de stockage ne parle que des chaînes - certains document stores, des APIs en forme de JSON, ou un schéma legacy que vous ne pouvez pas changer - et le prix est l'impôt de taille d'un tiers sur la colonne, plus la discipline de décoder à l'entrée et de ré-encoder à la sortie à chaque frontière.
Grandes entrées, mémoire stable
Le module est basé sur des buffers : un appel de décodage lit toute la chaîne d'un coup et renvoie tout le résultat. Il n'y a pas de décodeur en streaming dans la bibliothèque standard, donc le conseil honnête pour les gros payloads est de planifier la mémoire. La bonne nouvelle, c'est que le décodage ne fait qu'amincir les choses - la sortie est au plus les trois quarts de l'entrée - donc la chaîne d'entrée est votre seule grosse allocation.
Si un payload est assez gros pour vous inquiéter, vous pouvez le décoder par groupes de quatre caractères, parce que les groupes de quatre du Base64 sont autosuffisants et que le dernier groupe partiel porte son propre 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
Ça fonctionne sur une entrée propre, non enveloppée - les mêmes règles que strict_decode64 impose - parce qu'un groupe final isolé n'est valide qu'avec son padding présent. Pour les fichiers vraiment énormes, les archives de plusieurs gigaoctets et ce genre de choses, le pattern est de lire le fichier en tranches, de décoder chaque tranche, et de streamer les octets sur disque, pour que seule une tranche soit en mémoire à la fois.
One-liners pour le terminal
Vous n'avez pas besoin d'un fichier script pour décoder des choses dans le shell. Ruby peut require le module au vol :
ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world
Et pour les fichiers, passez un chemin de fichier au lieu du payload lui-même :
ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin
Deux pièges vivent ici. Premièrement, si vous pipez à travers echo ou n'importe quelle commande de texte, un retour à la ligne final voyage avec, et strict_decode64 lèvera dessus - utilisez decode64, ou chomp l'entrée :
echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'
Deuxièmement, gardez print au lieu de puts pour la sortie binaire, parce que puts ajoute un retour à la ligne de son propre chef et corromprait la fin de votre fichier restauré.
Les pièges que les développeurs Ruby rencontrent vraiment
- decode64 ne lève jamais. N'importe quoi entre, n'importe quoi sort. Si votre entrée n'est pas de confiance et que vous acceptez silencieusement des octets corrompus, le bug apparaîtra des semaines plus tard dans un fichier corrompu, pas sur la ligne de décodage. Partez d'un décodeur strict par défaut pour tout ce que vous n'avez pas encodé vous-même.
- strict_decode64 et le retour à la ligne final. Les fichiers texte, les pipes echo et le copier-coller aiment tous finir par une nouvelle ligne, et le décodeur strict lève
ArgumentErrordessus.chompd'abord l'entrée - ou lisez-la en mode binaire et supprimez les retours à la ligne. - Oublier l'étape d'encodage. Une chaîne décodée est BINARY jusqu'à ce que vous disiez le contraire. Forcez du UTF-8 (et vérifiez la validité) avant de traiter le résultat comme du texte, sinon vous aurez du mojibake et
Encoding::CompatibilityErrorle moment où vous la mêlez à des chaînes UTF-8. - Comparer du BINARY avec du UTF-8. Mêmes octets, étiquettes différentes, et
==dit false - à moins que la chaîne ne soit par hasard en ASCII pur. Rebadigez avant de comparer. - Entrée sûre pour les URL avec le mauvais décodeur. Les tirets et les tirets bas sont silencieusement jetés par
decode64, donc un payload sûr pour les URL revient un octet en moins et corrompu, sans la moindre erreur. Utilisezurlsafe_decode64. - Les données après le padding sont invisibles.
decode64s'arrête au premier=. Parfait pour le MIME, terrible pour attraper un payload qui a été tronqué puis re-padé par un autre outil. - Le padding non canonique est accepté silencieusement. Une chaîne comme
QR==porte des bits de padding qu'un encodeur correct aurait mis à zéro ;decode64la décode joyeusement tandis questrict_decode64la rejette. Rien ne vous dira jamais que votre encodeur mentait. - La lecture de fichiers en mode texte sous Windows réécrit les fins de ligne avant que vous ne les voyiez. Lisez les fichiers
.b64en mode binaire quand vous comptez les valider.
Bonnes habitudes côté décodage
- Choisissez le décodeur d'après la source des données :
strict_decode64pour tout ce qui n'est pas de confiance (et rescuezArgumentErrorcomme votre branche d'entrée invalide),urlsafe_decode64pour les payloads nés dans une URL,decode64seulement pour les formats vraiment indulgents, comme les corps MIME. - Dès que les octets sont décodés, décidez de leur identité : binaire (gardez ASCII-8BIT, écrivez avec
File.binwrite) ou texte (force_encodingvers UTF-8, puisvalid_encoding?avant usage). - Ne décodez jamais et ne faites jamais confiance. Un payload JWT est lisible précisément parce que c'est du Base64 ; c'est la signature qui décide s'il est authentique. Une chaîne Base64 dans un fichier de config est des données, pas une preuve.
- Quand vous écrivez des validateurs, testez-les sur les cas ennuyeux : la chaîne vide, l'entrée sans padding, l'entrée enveloppée, l'entrée sûre pour les URL, et le padding faux. Ce sont ces cas qui séparent les trois décodeurs.
Une brève histoire du Base64 en Ruby
Le module Base64 fait partie de la bibliothèque standard de Ruby depuis plus de quinze ans, et la façon dont il est livré a changé plus que vous ne pourriez le penser :
- 2008, Ruby 1.8.7 : le module arrive avec
encode64,decode64, plus deux méthodes qui n'existent plus -b64encode(enveloppage à une longueur de ligne choisie) etdecode_b(décodage d'en-têtes d'e-mail RFC 2047). Les vieux livres et même quelques vieilles gems les citent encore, et appeler l'une ou l'autre aujourd'hui, c'est unNoMethodError. - 2009, la ligne 1.9 :
strict_encode64,strict_decode64,urlsafe_encode64eturlsafe_decode64arrivent, et les deux méthodes legacy sont mises à la retraite (1.9.1 a déjà livré les deux changements en janvier 2009). - 2015, Ruby 2.3 :
urlsafe_encode64gagne le mot-clépadding:, vous laissant émettre une sortie sans padding pour les tokens et les URL. - 2020, Ruby 3.0 : base64 est extrait de la bibliothèque standard dans sa propre gem, version 0.1.0, sous le dépôt
ruby/base64. Il est livré comme default gem, doncrequire "base64"marche toujours sans cérémonie. - 2023, Ruby 3.3 : la version 0.2.0 ajoute
Base64::VERSIONet un ensemble de documentation bien plus riche. - 2024, Ruby 3.4 : la gem est reclassée de default gem en bundled gem. La conséquence pratique : dans les projets basés sur Bundler avec Ruby 3.4 ou plus récent, listez
gem "base64"dans votre Gemfile (ou installez-la avecgem install base64). - 2025, Ruby 4.0 : la version 0.3.0 atterrit, ajoutant des signatures de types RBS parmi d'autres travaux d'entretien.
À travers tout ça, un fait n'a jamais changé : le module est quelques dizaines de lignes de Ruby pur posées sur les gabarits pack et unpack du core. Pas d'extension C, pas de dépendances, rien à compiler - et un compteur de téléchargements dans les centaines de millions sur rubygems.org.
Anecdotes Ruby pour les curieux
- La partie décodage du module, c'est deux corps de méthodes d'une ligne,
str.unpack1("m")etstr.unpack1("m0"), plus la variante urlsafe, qui est un échange de lettres et une correction de padding par-dessus la stricte. Vous pouvez supprimer le require et l'écrire vous-même. - Le
Net::HTTPde Ruby lui-même n'utilise même pas le moduleBase64pour le Basic auth - il appelle le gabaritpackdirectement :["user:pass"].pack("m0"). - Les cookies signés et chiffrés de Rails sont des chaînes Base64 sous le capot : le codec de messages d'ActiveSupport choisit
strict_encode64pour les cookies ordinaires eturlsafe_encode64avecpadding: falsepour les IDs signés sûrs pour les URL. Vous en avez probablement décodé un sans le savoir. - Chaque classe de digest a une méthode
base64digest-Digest::SHA256.base64digest("hello")- un one-liner pour les checksums qui doivent vivre dans du texte. - Le tag
!binaryde YAML, c'est du Base64. Dump une chaîne BINARY avec Psych et le format fait l'encodage pour vous, discrètement. decode64se moque que vos lignes fassent 60, 64 ou 76 caractères, ou qu'il y ait une seule ligne géante. Le gabaritmsaute les retours à la ligne, donc entrées enveloppées et non enveloppées se décodent identiquement.
Pour aller plus loin
Vous avez maintenant la boîte à outils de décodage complète : un lecteur indulgent pour les blobs en forme MIME, un gardien strict pour tout ce qui n'est pas de confiance, un diplomate sûr pour les URL pour les tokens et les liens, et l'étape d'encodage qui transforme les octets résultants en du texte que Ruby vous laissera utiliser. Le sens inverse - décider dans quel des trois encodeurs de Ruby faire entrer vos octets, et contrôler l'alphabet, le padding et les retours à la ligne - porte sa propre série de surprises, en commençant par un retour à la ligne final que personne n'a demandé. Côté-là de la rue est couvert en profondeur dans l'article sur l'encodage Base64, lié ci-dessous.
Dernière mise à jour : 2026-09-08
Article associé : Encodage Base64 en Ruby : un guide complet