Encodage Base64 en Ruby : un guide complet
Vos données ont une destination qui n'accepte pas leur apparence. Une image qui doit vivre à l'intérieur d'un document JSON. Un token qui doit traverser une URL. Une pièce jointe qui doit survivre à un protocole conçu pour du texte à 7 bits. Un secret qui doit se poser dans une variable d'environnement sans casser le quoting. Dans chacun de ces endroits, quelque chose entre ici et là est sur le point de détruire votre binaire - et la solution a un nom : le Base64.
En Ruby, tout le travail vit dans un seul module livré avec le langage. Trois encodeurs, rien à installer, et une sortie que vous pouvez prédire au caractère près avant même d'avoir exécuté le code. Cette prévisibilité, c'est la moitié de l'histoire que la plupart des guides sautent, parce que c'est à l'encodage que les surprises se paient : un retour à la ligne final s'infiltre dans votre JSON, un retour à la ligne que vous n'avez pas demandé coupe un token en deux, et un seul mauvais choix d'alphabet ruine une URL. Ce guide passe en revue les trois encodeurs, le calcul de la sortie, et chaque payload qu'un développeur Ruby encode vraiment, pour que les surprises cessent d'être des surprises.
Un petit rappel avant de commencer : le Base64 réécrit les données trois octets à la fois, en émettant quatre caractères tirés d'un alphabet de 64 symboles, avec un ou deux caractères = de padding quand l'entrée ne se divise pas exactement par trois - c'est aussi pour ça que la sortie finit environ un tiers plus grosse que l'entrée. La page d'accueil de ce site couvre le format en profondeur, alors cet article tient les propos sur le format en un seul souffle et passe direct au travail.
Quel encodeur faut-il ?
Ruby vous donne trois encodeurs, et le choix entre eux tient en un quiz à trois questions : la sortie peut-elle contenir des retours à la ligne ? Peut-elle contenir + ou / ? Peut-elle contenir du padding ? Voici toute la troupe :
| Encodeur | Forme de la sortie | Retours à la ligne | Padding | À prendre quand |
|---|---|---|---|---|
Base64.strict_encode64(bin) |
une ligne, alphabet standard | jamais | toujours présent | JSON, tokens, APIs, fichiers - le défaut en sécurité |
Base64.encode64(bin) |
plusieurs lignes, alphabet standard | après tous les 60 caractères, plus un final | toujours présent | corps d'e-mail et autres protocoles de texte orientés lignes |
Base64.urlsafe_encode64(bin, padding: true) |
une ligne, alphabet tiret-tiret bas | jamais | votre choix, activé par défaut | tout ce qui atterrit dans une URL, un cookie ou un identifiant |
Si vous décidez sous pression du temps, la réponse courte est : strict_encode64 par défaut, urlsafe_encode64 quand le résultat voyagera dans une URL, et encode64 seulement quand le côté récepteur est un protocole de texte qui veut des lignes courtes. Tout ce qui suit explique pourquoi, et où chaque choix vous coûte quelque chose en silence.
strict_encode64 : le cheval de trait
Base64.strict_encode64 est l'encodeur que vous utiliserez vraiment dans la grande majorité de votre code. Il produit exactement une ligne de sortie, toujours avec le bon padding, depuis l'alphabet standard :
require "base64"
Base64.strict_encode64("hello world")
# => "aGVsbG8gd29ybGQ="
Base64.strict_encode64("s")
# => "cw=="
Et comme l'algorithme est déterministe, vous pouvez prédire la longueur exacte de la sortie à partir de l'entrée - pas de devinettes, pas de bugs d'off-by-one dans vos colonnes de base de données. Le tableau ci-dessous est tout l'arithmétique :
| Longueur d'entrée | Longueur de sortie | Padding à la fin |
|---|---|---|
| 3n octets (se divise exactement) | 4n caractères | aucun |
| 3n + 1 octets | 4n + 4 caractères | deux = |
| 3n + 2 octets | 4n + 4 caractères | un = |
Donc 11 octets deviennent 16 caractères, 100 octets deviennent 136, et un fichier de 1 mégaoctet devient environ 1,33 mégaoctet de texte. Cette croissance d'un tiers est le prix d'entrée de chaque payload Base64 que vous expédiez, et c'est le nombre à garder dans la poche arrière quand une colonne, un cache ou une limite de débit d'API commence à serrer.
Base64.strict_encode64("123")
# => "MTIz" 3 octets en entrée, 4 caractères en sortie
Base64.strict_encode64("1234")
# => "MTIzNA==" 4 octets en entrée, 8 caractères en sortie, deux caractères de padding
Base64.strict_encode64("12345")
# => "MTIzNDU=" 5 octets en entrée, 8 caractères en sortie, un caractère de padding
encode64 : celui qui ajoute des retours à la ligne
Base64.encode64 est le classique, et il a un comportement qui a mis fin à plus d'un après-midi : il enveloppe sa sortie. Tous les 60 caractères, il commence une nouvelle ligne, et il finit toujours avec un retour à la ligne final :
Base64.encode64("hello world")
# => "aGVsbG8gd29ybGQ=\n"
Base64.encode64("*" * 46)
# => "KioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioqKioq\nKg==\n"
L'enveloppage n'est pas un bug - c'est une fonctionnalité héritée de la maison d'origine de la méthode dans le monde MIME, où les longues lignes étaient une violation du protocole. La gem mail de Ruby s'appuie dessus exprès - son encodeur Base64 porte même un commentaire disant que l'enveloppage automatique des lignes de Ruby garde la sortie dans les limites de longueur de ligne du SMTP. Si vous encodez des corps d'e-mail, encode64 vous fait une faveur.
Mais dans tous les autres contextes, l'enveloppage est un impôt. L'accident le plus courant, c'est un document JSON où une valeur Base64 s'étend tout à coup sur deux lignes :
payload = { "logo" => Base64.encode64(File.binread("logo.png")) }
puts payload.to_json
# la valeur logo porte des retours à la ligne que personne n'a demandés
Et la jumelle en plus petit du même bug, c'est le retour à la ligne final sur les chaînes courtes : Base64.encode64("s") renvoie "cw==\n", donc un token que vous collez dans une URL ou comparez à une valeur attendue échoue pour des raisons que vous passerez vingt minutes à traquer. Le remède, c'est un strip - mais le meilleur remède, c'est strict_encode64, qui n'ajoute jamais le moindre caractère que vous n'avez pas gagné. Il y a aussi une charmante asymétrie à connaître : une entrée vide produit une chaîne vide sans retour à la ligne final, donc Base64.encode64("") est juste "".
urlsafe_encode64 : l'alphabet sûr pour les liens
Deux caractères de l'alphabet standard causent des problèmes partout où un parseur d'URL regarde : + (une espace, dans les chaînes de requête) et / (un séparateur de chemin). La RFC 4648 a réglé ça avec un échange - - prend la place de +, _ prend la place de / - et Ruby l'implémente dans Base64.urlsafe_encode64 :
Base64.urlsafe_encode64("\xfb\xef\xbe".b)
# => "----"
Base64.urlsafe_encode64("\xff\xff\xff".b)
# => "____"
Ces deux exemples, c'est l'alphabet en vedette : les mêmes octets que l'encodeur standard rend en ++++ ou //// sortent en ---- et ____, des caractères qui survivent aux URLs, aux chemins, aux noms de fichiers et aux champs de formulaire sans aucun percent-encoding. La sortie fait une ligne, comme strict_encode64.
L'unique option de la méthode, c'est le mot-clé padding:, ajouté en Ruby 2.3, et c'est celui qu'il faut connaître. La spécification JSON Web Token exige du base64url sans padding, et beaucoup d'autres schémas de tokens aussi :
Base64.urlsafe_encode64("*")
# => "Kg=="
Base64.urlsafe_encode64("*", padding: false)
# => "Kg"
Padding éteint, le calcul de longueur bouge : 3n + 1 octets donnent maintenant 4n + 2 caractères, et 3n + 2 octets donnent 4n + 3. Le côté décodeur s'en accommode - le urlsafe_decode64 de Ruby ajoute lui-même le padding manquant - donc une sortie sans padding est sûre à émettre, mais la sortie avec padding est le défaut plus amical quand l'autre côté est un lecteur strict RFC 2045. Une mise en garde : éteignez le padding seulement quand une spécification l'exige. Ça économise un ou deux caractères et vous achète une classe de plaintes de décodeurs.
Ce que Ruby encode vraiment : les chaînes sont des octets
Avant les cas d'usage, un fait propre à Ruby qui façonne tout : une chaîne Ruby est une séquence d'octets portant une étiquette d'encodage, et les encodeurs ne regardent que les octets. L'étiquette dit à Ruby comment afficher et comparer la chaîne ; elle ne change pas ce qui est encodé :
require "base64"
s = "h\u{e9}llo"
puts s.encoding
# => UTF-8
puts s.bytes.length
# => 6 le e accentué fait deux octets
Base64.strict_encode64(s)
# => "aMOpbGxv"
C'est le piège derrière « pourquoi ma sortie est plus longue que prévu » : la chaîne que vous avez tapée est d'habitude plus courte en caractères qu'en octets, et le Base64 facture par octet. Le sens inverse est tout aussi silencieux - une chaîne UTF-8 invalide est encodée sans la moindre plainte, parce que l'encodeur n'a rien à valider :
broken = "h\u{e9}llo".b.force_encoding("UTF-8")
broken.setbyte(1, 0xFF)
puts broken.valid_encoding?
# => false
Base64.strict_encode64(broken)
# => du base64 quelconque, pas d'erreur, des octets sont des octets
Pour du vrai binaire, sautez toute la machinerie de texte et construisez les octets avec pack ou lisez-les avec File.binread. Un exemple satisfaisant, c'est la signature PNG - les huit octets qui ouvrent chaque fichier PNG sur Terre :
png_magic = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A].pack("C*")
Base64.strict_encode64(png_magic)
# => "iVBORw0KGgo="
JWTs : signer des données qui sont aussi lisibles
Les JSON Web Tokens sont le consommateur le plus célèbre de l'encodeur sûr pour les URL de Ruby. Un token, c'est trois segments base64url reliés par des points - en-tête, payload, signature - et la spécification est explicite : l'alphabet doit être celui sûr pour les URL, et le padding doit être éteint. La gem jwt s'occupe de tout :
# Gemfile: gem "jwt"
require "jwt"
token = JWT.encode(
{ sub: "1234567890", name: "Alice", exp: Time.now.to_i + 3600 },
"my-secret-key",
"HS256"
)
puts token
# => eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIi...
payload, header = JWT.decode(token, "my-secret-key", true, algorithm: "HS256")
puts header
# => {"alg"=>"HS256"}
Vous pouvez aussi regarder la couche Base64 faire son travail à l'intérieur du token, parce que les segments ne sont que du base64url de JSON :
require "base64"
require "json"
payload_json = JSON.generate({ "sub" => "1234567890", "name" => "Alice" })
segment = Base64.urlsafe_encode64(payload_json, padding: false)
puts segment
# => eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkFsaWNlIn0
Deux règles vont avec ce cas d'usage. Ne fabriquez jamais votre JWT à la main en production - c'est la signature qui fait d'un token autre chose qu'une confession - et quand vous décodez avec la gem, épinglez l'algorithme dans le hash d'options comme montré plus haut, pour que l'en-tête du token lui-même ne choisisse pas la méthode de vérification à votre place.
HTTP Basic auth : construire l'en-tête
Le plus vieux moyen de dire « qui suis-je » en HTTP est aussi le plus simple : encoder les identifiants en Base64, les mettre après le mot Basic, et envoyer l'en-tête. Le construire en Ruby tient en une ligne :
require "base64"
credentials = Base64.strict_encode64("alice:s3cr3t!")
puts "Basic #{credentials}"
# => Basic YWxpY2U6czNjcjN0IQ==
La bibliothèque standard de Ruby fait exactement ça pour vous dans Net::HTTP, en appelant le gabarit pack du core directement - ["user:pass"].pack("m0"), c'est ce à quoi basic_auth se réduit sous le capot :
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
Et la mise en garde de sécurité, dite une fois pour qu'elle soit au dossier : le Base64 est un traducteur, pas un cadenas. Les identifiants d'un en-tête Basic auth sont lisibles par n'importe qui qui peut lire le paquet. Cet en-tête n'est acceptable que sur HTTPS, où c'est le transport qui fait la vraie protection.
Data URIs : images et polices en ligne
Une data URI, c'est la réponse du web à « je veux cette image sans fichier séparé » : un type média, le mot base64, une virgule, et les octets. C'est comme ça que les démos HTML monofichier expédient leurs logos, que les favicons se cachent dans le CSS, et qu'une image générée peut vivre entièrement dans une chaîne de template :
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
css = "background-image: url(#{data_uri});"
puts css.length
# => votre feuille de style, moins une requête HTTP
Utilisez strict_encode64 ici - le payload est une seule ligne propre, sans enveloppage, sans retour à la ligne. Et surveillez la taille : l'image que vous intégrez grossit d'environ un tiers, donc les data URIs brillent pour les petits assets (favicons, logos, polices d'icônes) et gonflent pour les gros. Une photo hero de deux mégaoctets devient 2,7 mégaoctets de votre document HTML, et vos utilisateurs le sentiront dès leur premier scroll en 4G.
E-mail : d'où vient le Base64
Tous les autres cas d'usage de cet article sont des descendants de celui-là. Le SMTP a été conçu dans les années 80 pour de courtes lignes de texte à 7 bits, ce qui veut dire qu'il ne pouvait pas porter un JPEG. La correction - la Privacy-Enhanced Mail, puis le MIME en 1993 - consistait à réécrire le binaire en texte avec un alphabet de 64 symboles, exactement le format que vous utilisez aujourd'hui. Les cicatrices sont toujours visibles dans la sortie de Ruby : encode64 enveloppe à 60 caractères - une largeur qui ne sert aucun protocole en particulier, comme vous le verrez plus tard, mais assez courte pour garder l'e-mail poli.
En pratique, vous laisserez la gem mail faire le travail MIME. Attachez un fichier binaire et la gem choisit l'encodeur Base64, enveloppe les lignes, et écrit les en-têtes :
# 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 texte non-ASCII dans les en-têtes reçoit le même traitement dans un costume légèrement différent : les mots encodés RFC 2047, qui enveloppent le Base64 dans un tag de charset entre points d'interrogation, comme =?UTF-8?B?w7wgc2VjcmV0cw==?=. Si un jour vous construisez ou parsez ceux-là à la main, le Base64 à l'intérieur est le genre ordinaire, décodé avec decode64 puis retiqueté avec le charset que le mot déclare.
L'armure PEM des clés et des certificats
Les clés et les certificats portent une armure PEM, et l'armure, c'est du Base64 avec un cadre : une ligne BEGIN, les octets encodés en lignes de 64 caractères, et une ligne END. Si un jour vous devez produire un fichier PEM à partir d'octets DER bruts, la construction tient en un enveloppage en deux temps :
require "base64"
der_bytes = File.binread("server.der")
body_lines = Base64.strict_encode64(der_bytes).scan(/.{1,64}/)
pem = (["-----BEGIN PRIVATE KEY-----"] + body_lines +
["-----END PRIVATE KEY-----"]).join("\n") + "\n"
File.write("server.key", pem)
Deux notes. Premièrement, vous en aurez presque jamais besoin, parce que la gem openssl écrit le PEM pour vous (key.to_pem), et le libellé entre les lignes BEGIN et END doit correspondre à ce qui est dedans - se tromper produit un fichier que tous les outils de l'internet refusent. Deuxièmement, la longueur de ligne ici, c'est 64, la largeur classique du PEM ; le encode64 de Ruby enveloppe à 60, lui, et tout parseur PEM décent ignore les longueurs de lignes complètement, donc les deux largeurs se décodent sans problème.
Fichiers : la convention .b64
Le format de fichier le plus courant du monde Base64 est un simple fichier texte avec une extension .b64 (ou .base64) contenant un payload encodé - pensez-y comme à « le fichier, mais sûr à coller n'importe où ». En produire un depuis Ruby tient en un one-liner :
require "base64"
File.write("payload.b64", Base64.strict_encode64(File.binread("payload.bin")))
puts File.size("payload.b64")
# => environ 1,33 fois la taille d'origine
Utilisez strict_encode64 pour que le fichier tienne une seule ligne propre - la convention que la plupart des outils de décodage (et le décodeur strict de Ruby) attendent. La relecture est l'image miroir : lire, décoder, et écrire les octets en mode binaire pour que rien ne les mute en chemin :
encoded = File.read("payload.b64")
bytes = Base64.strict_decode64(encoded)
File.binwrite("restored.bin", bytes)
Si vos fichiers .b64 viennent d'outils qui enveloppent les lignes - certaines variantes du CLI base64 le font - retirez les retours à la ligne avant un décodage strict, ou utilisez le décodeur indulgent, qui les saute gratuitement.
Fichiers de config, variables d'environnement et bases de données
Chaque fois que des données binaires doivent vivre dans un document texte, le Base64 est le pont. Le pattern se répète en trois endroits avec de petites variations.
Les variables d'environnement et les fichiers .env ne peuvent pas tenir d'octets bruts, alors les octets sont encodés avant de quitter la machine qui les a :
require "base64"
# quelque part où vous provisionnez l'application
ENV["APP_LOGO"] = Base64.strict_encode64(File.binread("logo.png"))
# quelque part où l'application démarre
b64 = ENV.fetch("APP_LOGO")
File.binwrite("logo.png", Base64.decode64(b64))
Le YAML a un type binaire natif, et Psych gère le Base64 pour vous - une chaîne BINARY dumpée en YAML sort comme un scalaire !binary, et se recharge identique à l'octet près :
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 question, c'est le type de stockage, pas l'encodage. Si votre base de données a une vraie colonne binaire - BLOB, BYTEA, VARBINARY - utilisez-la, et laissez le driver porter les octets. Le Base64-dans-une-colonne-TEXT est le pattern pour 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. Le prix, c'est l'impôt de taille d'un tiers sur la colonne, et la discipline d'encoder à l'entrée et de décoder à la sortie à chaque frontière, sans exception.
Des checksums qui voyagent en texte
Les hashes sont binaires, mais les checksums voyagent surtout en texte : listes d'intégrité de fichiers, clés de cache, empreintes, lignes de log. Chaque classe de digest de Ruby a une méthode base64digest qui fait l'encodage en un seul appel :
require "digest"
Digest::SHA256.base64digest("hello")
# => "LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="
La sortie est du Base64 standard avec padding - exactement ce que donnerait Base64.strict_encode64(Digest::SHA256.digest("hello")) - donc c'est sûr à stocker, à comparer et à coller. La seule décision, c'est la cohérence : une liste de checksums générée en Base64 doit être vérifiée contre des sorties Base64, et les représentations hex et Base64 du même hash sont des chaînes différentes, alors choisissez-en une et tenez-y vous.
Encoder les grosses choses en petits morceaux
Comme les décodeurs, les encodeurs sont basés sur des buffers : ils lisent toute l'entrée et émettent toute la sortie. Il n'y a pas d'encodeur en streaming dans la bibliothèque standard, donc pour les gros payloads, le plan, c'est la mémoire, et il y a une agréable symétrie dans le calcul. L'encodage grossit vos données d'un tiers, donc la sortie - pas l'entrée - est votre plus grosse allocation, et pour un fichier d'un gigaoctet, attendez-vous à environ 1,33 gigaoctet de texte devant vous.
Si c'est trop pour tenir d'un coup, vous pouvez encoder par morceaux, parce que l'alphabet du Base64 est auto-synchronisé sur les frontières de trois octets : encodez chaque tranche de 3 octets indépendamment et la concaténation est identique à l'encodage du tout :
require "base64"
require "securerandom"
bin = SecureRandom.random_bytes(10_001)
whole = Base64.strict_encode64(bin)
chunked = bin.scan(/.{1,3}/m).map { |slice| Base64.strict_encode64(slice) }.join
puts chunked == whole
# => true
Le même tour vous donne un enveloppeur de lignes fait main qui correspond exactement à encode64 : 45 octets encodent toujours exactement 60 caractères, donc trancher l'entrée à 45 octets et relier les morceaux avec des retours à la ligne reproduit la sortie MIME classique, ligne par ligne, avec seulement une tranche en mémoire à la fois :
def wrap_like_encode64(bin)
lines = bin.scan(/.{1,45}/m).map { |slice| Base64.strict_encode64(slice) }
lines.join("\n") + "\n"
end
bin = SecureRandom.random_bytes(10_001)
puts wrap_like_encode64(bin) == Base64.encode64(bin)
# => true
Depuis la ligne de commande
L'encodage non plus n'a pas besoin d'un fichier script. La forme one-liner lit un fichier et écrit son Base64 sur stdout :
ruby -rbase64 -e 'print Base64.strict_encode64(File.binread(ARGV[0]))' payload.bin > payload.b64
Et la forme en pipe lit stdin, c'est comme ça que vous envelopperiez un flux d'octets de n'importe quelle autre commande :
some_command | ruby -rbase64 -e 'print Base64.strict_encode64(STDIN.read)'
Gardez print dans les deux - un puts errant ajouterait un retour à la ligne à votre Base64, et pour une sortie de strict_encode64, ça transforme un token propre en token cassé. La même règle que du côté du décodage : si le prochain consommateur de votre sortie est strict, rien d'autre que le Base64 lui-même n'a le droit de voyager avec.
Les pièges qui coûtent des octets en plus aux développeurs Ruby
- Le retour à la ligne final dans le JSON.
Base64.encode64termine chaque résultat non vide par un retour à la ligne, donc une valeur qui devrait être un token propre arrive dans votre JSON avec un\nde surprise à la fin. Utilisezstrict_encode64pour tout ce qui sera stocké, comparé ou envoyé en une seule ligne. - L'enveloppage à 60 caractères dans les tokens et les URL. La même méthode enveloppe les longues sorties sur plusieurs lignes. Une chaîne enveloppée dans une URL, ce sont deux URLs, et un token enveloppé, c'est un token cassé. Encore :
strict_encode64, oustrip/deleteles retours à la ligne si vous êtes coincés avec une sortie deencode64. - Le plus et le slash dans les URL. Du Base64 standard dans une chaîne de requête, c'est du percent-encoding
%2B,%2Fet%3Dà la sortie, et prier pour que l'autre côté les décode.urlsafe_encode64supprime le problème à la source. - Le padding au mauvais endroit. Les JWTs et les autres schémas de tokens veulent le padding éteint ; les lecteurs MIME peuvent ne pas s'accommoder du padding manquant. Émettez
padding: falseseulement là où une spécification l'exige, et sachez de quel côté de la clôture se trouve chacun de vos consommateurs. - Les caractères ne sont pas des octets. Une chaîne de cinq caractères avec une lettre accentuée, c'est six octets en UTF-8, et le calcul de la longueur de sortie tourne sur les octets. Quand le résultat encodé est « trop long », comptez des octets, pas des caractères.
- L'impôt d'un tiers dans la conception de schémas. Un BLOB de 16 KB devient une chaîne Base64 d'environ 22 KB dans une colonne TEXT. Dimensionnez vos colonnes, vos caches et vos payloads d'API pour la forme encodée, pas la forme binaire.
- Deux alphabets, deux chaînes différentes. Les mêmes octets s'encodent différemment dans les alphabets standard et sûr pour les URL, donc une valeur encodée n'est comparable qu'à une autre valeur du même alphabet. Ne les comparez jamais et ne les mélangez jamais.
- Le Base64 n'est pas un cadenas. Encoder un secret ne le rend pas secret. N'importe qui qui a la chaîne a vos données ; le Base64 ne contrôle que l'apparence des octets, pas qui peut les lire.
Les habitudes qui font économiser des octets et des bugs
- Faites de
strict_encode64votre défaut. Passez àurlsafe_encode64au moment où la sortie vivra dans une URL, un cookie ou un identifiant, et àencode64seulement quand la destination est un protocole de texte orienté lignes comme l'e-mail. - Gardez l'alphabet cohérent entre l'encodeur et le décodeur aux deux bouts du fil. Le bug « Base64 cassé » le plus courant du monde, c'est un producteur d'alphabet standard qui rencontre un consommateur sûr pour les URL, ou l'inverse.
- Donnez aux encodeurs des octets que vous comptez vraiment encoder :
File.binreadpour les fichiers,packpour le binaire construit, et une chaîne UTF-8 quand la chaîne est la donnée. L'encodeur ne remettra pas vos choix en question - il compte juste des octets. - Budgétisez la croissance. Chaque fois qu'une chaîne Base64 franchit une frontière vers un conteneur de taille fixée, multipliez par 4/3 et ajoutez un peu de marge pour le padding.
- Utilisez le Base64 pour la portabilité, jamais pour le secret. Si le but est de garder les données privées, l'outil, c'est le chiffrement, et le Base64 n'est que ce que vous faites avec le texte chiffré ensuite.
Comment le Base64 est devenu une gem
Pendant l'essentiel de sa vie, le module Base64 n'était qu'un fichier de la bibliothèque standard, comme beaucoup des plus vieux helpers de Ruby. Les méthodes strict et sûre pour les URL ont rejoint le duo d'origine pendant la ligne de développement 1.9 - la bibliothèque base64 entière, avec ses quatre méthodes, a été ajoutée au trunk en septembre 2008 et livrée pour la première fois en 1.9.1 (2009), et le mot-clé padding: est arrivé avec Ruby 2.3 en 2015. Tout ce que vous voyez aujourd'hui dans l'API était déjà en place à ce moment-là - le reste de l'histoire, c'est la façon dont le module est livré.
En 2020, avec Ruby 3.0, l'équipe core a commencé à extraire les bibliothèques standard dans leurs propres gems, et base64 est devenu l'une d'elles : version 0.1.0, maintenue dans le dépôt ruby/base64 par les contributeurs du core. Elle était livrée comme default gem - distribuée avec Ruby et toujours disponible, donc require "base64" continuait de marcher sans aucune cérémonie. La version 0.2.0 a suivi avec Ruby 3.3 en 2023, ajoutant la constante Base64::VERSION et un ensemble de documentation bien plus riche.
Puis Ruby 3.4 en décembre 2024 a redessiné la ligne : base64 est passé de la liste des default gems à celle des bundled gems, la même étagère que csv et drb. Les bundled gems sont toujours livrées avec le langage, mais les projets basés sur Bundler sont censés les déclarer, donc si vous êtes sur Ruby 3.4 ou plus récent et que votre appli est pilotée par Bundler, ajoutez gem "base64" à votre Gemfile (ou exécutez gem install base64) et vous êtes couvert. Ruby 4.0 en 2025 a apporté la version 0.3.0, avec des signatures de types RBS pour que les vérificateurs statiques voient le module correctement.
Tout au long du voyage, l'implémentation est restée ce qu'elle a toujours été : quelques dizaines de lignes de Ruby pur enveloppées autour des gabarits pack et unpack du core. Pas d'extension C, pas de dépendances, et - avec un compteur de téléchargements dans les centaines de millions sur rubygems.org - l'une des gems les plus installées de la plateforme.
Faits amusants sur Ruby
- Le module entier, encodeurs compris, est assez court pour se lire pendant une pause café.
encode64, c'est littéralement[bin].pack("m"),strict_encode64, c'est[bin].pack("m0"), eturlsafe_encode64, c'est l'encodeur strict avec un échange de deux lettres par-dessus, moins le padding quand vous le demandez. - L'enveloppage à 60 caractères de
encode64ne correspond ni au maximum de 76 caractères du MIME ni au 64 classique du PEM. C'est simplement ce que le gabarit packma toujours fait, et l'encodeur Base64 de la gemmaily fait allusion avec approbation : l'enveloppage automatique des lignes de Ruby garde la sortie dans les limites du SMTP. - Le
Net::HTTPde Ruby ne se donne même pas la peine d'utiliser le moduleBase64pour le Basic auth - il appelle le gabaritpackdirectement, ce qui est un bon rappel que le module est une couche de confort par-dessus le core, et pas l'inverse. - Chaque classe de digest porte une méthode
base64digest, doncDigest::SHA256.base64digestest un citoyen de première classe à côté dehexdigest- des checksums en texte sans second appel. - Le tag
!binaryde YAML, c'est du Base64 déguisé. Psych fait l'encodage dès que vous dump une chaîne BINARY, c'est pour ça que les fichiers de config pleins de binaires ont l'allure qu'ils ont. - Le module que vous utilisez n'a pas toujours été le module que vous vous souvenez. Le vieux Ruby avait
b64encode(enveloppage à une largeur choisie) etdecode_b(décodage d'en-têtes RFC 2047) ; les deux ont disparu dans la ligne 1.9, donc tout le code d'avant 2010 que vous héritez et qui les appelle meurt avec unNoMethodError. - Les IDs de vidéo de YouTube sont du base64url sans padding - onze caractères, pas de plus, pas de slash, pas d'égal - exactement le genre d'identifiant court et sûr pour les liens pour lequel l'alphabet sûr pour les URL a été conçu.
L'autre face
Vous avez maintenant le tableau complet de l'encodage : un cheval de trait par défaut qui ne vous surprend jamais, un classique qui enveloppe les lignes pour les protocoles qui le demandent, un alphabet sûr pour les liens avec un interrupteur de padding, et les règles au niveau des octets qui décident exactement à quoi ressemblera votre sortie. Le sens inverse - démélanger une chaîne Base64, choisir entre les trois décodeurs de Ruby, et transformer les octets résultants en quelque chose que vous pouvez utiliser - a ses propres pièges silencieux, en commençant par un décodeur qui ne dit jamais non. Côté-là de la rue est couvert en profondeur dans l'article sur le décodage Base64, lié ci-dessous.
Dernière mise à jour : 2026-09-08
Article associé : Décodage Base64 en Ruby : un guide complet