Base64-decodering in Ruby: een complete gids
Iergens tussen jou en de oorspronkelijke data staat een muur van tekens: hoofd- en kleine letters, cijfers, misschien een plus of een slash of een koppelteken, en mogelijk een gelijkteken dat achteraan geparkeerd staat. Je editor heeft geen idee wat voor bestandstype dat is. Je database stopte het in een tekstkolom. Het arriveerde in een HTTP-header, een URL, een YAML-sleutel, of een supportticket met een .b64-bijlage. Je herkent het in een oogwenk - Base64 - en nu heb je de bytes terug nodig. In Ruby is die behoefte één require-statement en één methodeaanroep verwijderd.
Moet je het formaat nog niet kennen, dan is dit de versie van dertig seconden. Base64 schrijft ruwe data om in stappen van drie bytes: elke groep van drie bytes wordt vier tekens uit een alfabet van 64 symbolen, en als de invoer niet exact door drie deelbaar is, komen er een of twee =-tekens bij als vulling, zodat de uitvoer altijd op een veelvoud van vier uitkomt. Decoderen is de reis in omgekeerde richting - vier tekens binnen, drie bytes buiten - en daarom is het resultaat altijd kleiner dan de invoer, ongeveer driekwart van de grootte. De startpagina van deze site doorloopt elk detail van het formaat, dus besteedt deze gids zijn energie daar waar die thuishoort: aan de Ruby-kant van het werk.
Het goede nieuws: elke Ruby-installatie levert het complete decoderingsgereedschap mee. De Base64-module heeft niets geïnstalleerd nodig, en zijn drie decoders zijn zo klein dat je de hele broncode in één zit leest. De waarschuwing: de decoder waar je het eerst naar grijpt, is ook degene die nooit klagt - een prachtige eigenschap voor e-mail, een vreselijke voor de veiligheid. Aan het einde van deze gids weet je precies wat elke decoder accepteert, hoe je de bytes die hij teruggeeft omzet in tekst die Ruby je toestaat te gebruiken, en wat je doet met elke payload die een Ruby-ontwikkelaar daadwerkelijk decodeert - JWT's, auth-headers, data-URI's, e-maillichamen, PEM-pantsering, bestanden, config-blobs en enorme.
Maak kennis met de gereedschapskist
Alles begint met een require. Geen installatiestap, geen platform-eigenaardigheden, geen native extensie om te bouwen:
require "base64"
puts Base64::VERSION
# => 0.2.0 op een standaard Ruby 3.3, bijvoorbeeld
Hier is de hele decoderingskant van de gereedschapskist in één tabel, in de volgorde waarin je naar elke methode zult grijpen:
| Decoder | Omgaan met vreemde tekens | Regels voor de vulling | Als iets niet klopt |
|---|---|---|---|
Base64.decode64(str) |
negeert alles wat niet in het standaardalfabet zit, inclusief regeleinden en spaties | alles, zelfs verkeerde vulling | niets - het gooit nooit een fout, het geeft gewoon terug wat het wist te decoderen |
Base64.strict_decode64(str) |
wijst elk teken buiten het standaardalfabet af | moet aanwezig zijn en exact goed | goot een ArgumentError |
Base64.urlsafe_decode64(str) |
accepteert het URL-veilige alfabet en het standaardalfabet, wijst alles andere af | optioneel, maar als hij er is moet hij correct zijn | goot een ArgumentError |
Wil je weten wat je gereedschap onder de motorkap doet, dan is de hele decoderingskant van de module een dunne wrapper om twee templates van de core pack/unpack-machinerie, die in C is geïmplementeerd in de Ruby-core:
# de hele decoderingskant van de module, in samengevatte vorm
def decode64(str)
str.unpack1("m")
end
def strict_decode64(str)
str.unpack1("m0")
end
De m-template is de tolerante lezer, m0 de strikte, en dat ene tekenverschil verklaart het hele persoonlijkheidsverschil tussen de eerste twee decoders. Omdat de zware klus op coresnelheid gebeurt, blijft de module zuivere Ruby en kauwt hij megabytes op in enkele milliseconden.
decode64: de camaleont
Base64.decode64 is de decoder die tegen alles ja zegt. Geef hem een nette payload en hij decodeert die. Geef hem een MIME-achtige blob vol regeleinden en hij schouwt. Geef hem een tekenreeks die helemaal geen Base64 is en hij geeft terug wat hij eruit kan persen, zonder een enkele waarschuwing:
require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"
Die tweede regel is het hele karakter in één voorbeeld. De decoder slaat alles over wat niet deel uitmaakt van het standaardalfabet - regeleinden, spaties, hier en daar een controleteken - en decodeert de rest. Dit is precies het gedrag dat MIME Base64 moet hebben, en daarom is decode64 het juiste gereedschap voor alles wat door e-mail gereisd is.
De keerzijde is wat het gevaarlijk maakt. Omdat de decoder nooit klagt, vertelt hij je ook nooit wanneer de invoer fout was:
Base64.decode64("not base64 at all!")
# => tien bytes perfect plausibel ogende rommel
Base64.decode64("====")
# => ""
Het eerste voorbeeld vindt de tekens die toevallig geldige alfabetletters zijn, decodeert ze, en geeft bytes terug die je misschien in de verleiding zou komen rechtstreeks naar een bestand te schrijven. Het tweede voorbeeld geeft een lege tekenreeks terug voor een tekenreeks van vier vultekens. Er wordt niets gegooid en niets gelogd. Als je invoer niet te vertrouwen is, is die stilte een eigenschap die je uit wilt zetten - en daar zijn de volgende twee decoders voor.
Nog een eigenaardigheid die het kennen waard is, want dit is het soort ding dat maandenlang in productie blijft verstoppen: decoderen stopt bij het eerste =-teken. Alles na de vulling is geen fout; het wordt simpelweg nooit gelezen:
Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello" het "Zm9vYmFy"-deel is onzichtbaar voor de decoder
strict_decode64: de poortwachter
Base64.strict_decode64 is de decoder met een checklist. Hij accepteert alleen het standaardalfabet (A tot Z, a tot z, 0 tot 9, plus, slash), hij eist dat eventuele vulling exact goed is, en hij weigert een enkele byte te produceren zodra een regel wordt geschonden:
Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => gooit ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => gooit ArgumentError
De laatste regel is de beslissende: dezelfde payload die decode64 met plezier decodeerde, gooit nu een uitzondering om één enkele regeleinde. Ontbrekende vulling, extra vulling, een koppelteken, een underscore, een spatie - elk is een misdaad, en de hele payload gaat eronder:
begin
Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
puts e.message
end
# => invalid base64
De poortwachter bewaakt zelfs hoeken van het formaat die je niet zoudt controleren. Wanneer een Base64-tekenreeks eindigt met vulling, worden sommige bits van het laatste teken nooit gebruikt, en zegt de RFC dat een conforme encoder die bits op nul moet zetten. Ruby controleert:
Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => gooit ArgumentError (de pad-bits zijn niet nul)
Het tweede teken zou naar dezelfde byte decoderen als het eerste als de decoder slordig was. Ruby is niet slordig. In de praktijk maakt dit strict_decode64 de juiste standaard voor elke invoer die je niet zelf hebt geëcodeerd: het zet typefouten, afkapping en het verkeerde alfabet om naar luidruchtige, vangbare fouten in plaats van stille corruptie.
urlsafe_decode64: de diplomaat
Base64.urlsafe_decode64 bestaat voor payloads die reizen naar plekken waar + en / reserve-woorden zijn: URLs, tokens, database-identifiers. Intern vertaalt het het URL-veilige alfabet (koppelteken en underscore) terug naar het standaardalfabet, normaliseert de vulling, en geeft het resultaat door aan de strikte decoder:
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => gooit ArgumentError (vijftien tekens hebben één vulteken nodig, niet twee)
Het eerste voorbeeld toont de handigste trek: invoer zonder vulling mag. Als de tekenreeks geen vulling heeft en de lengte geen veelvoud van vier is, voegt de decoder de ontbrekende =-tekens voor je toe - precies wat JSON Web Tokens, de grootste consument van URL-veilige Base64, produceren. Als er wél vulling aanwezig is, moet die correct zijn, net zoals bij de strikte decoder.
Er is één eigenaardigheid die de documentatie niet met klem aankondigt: de diplomaat spreekt beide talen. Omdat de methode koppeltekens en underscores eerst herschrijft voordat hij strikt decodeert, accepteert hij ook tekenreeksen uit het standaardalfabet:
Base64.urlsafe_decode64("aGVsbG8=")
# => "hello" het standaardalfabet wordt ook geaccepteerd
Die tolerantie is handig, maar het betekent dat je deze methode niet kunt gebruiken om te zien uit welk alfabet een payload komt. Als dat voor je van belang is, inspecteer dan zelf de tekens voordat je decodeert.
En in tegenstelling tot decode64 heeft de diplomaat geen genade voor witruimte. Een regeleinde ergens in een URL-veilige payload gooit ArgumentError, dus als je invoer uit een omwikkeld bestand komt, haal dan eerst de regeleinden eraf.
Bytes zijn geen tekst: de coderingsstap
Deze stap is waar ook ervaren ontwikkelaars over struikelen, omdat Ruby hem zichtbaar maakt. Een gedecodeerde Base64-tekenreeks draagt altijd de ASCII-8BIT-codering (ook bekend als BINARY), of de oorspronkelijke data nu een PNG was, een JWT-payload, of een liefdesbrief in 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]
Als de payload binair is - een afbeelding, een zip-bestand, een hash - houd je het exact zo en schrijf je het weg met File.binwrite. Geen conversie, geen vragen. Als de payload tekst is, zijn de bytes vrijwel zeker UTF-8, en moet je Ruby dat laten weten:
text = Base64.decode64(payload)
text.force_encoding("UTF-8")
if text.valid_encoding?
puts text
else
puts "not valid UTF-8 after all"
end
De twee aanroepen doen verschillende dingen. force_encoding plakt alleen een nieuw label op de bytes; valid_encoding? controleert daarna of ze echt UTF-8 vormen. Voer ze in die volgorde uit, want een BINARY-tekenreeks eerst valideren heeft niets om te valideren. En een kleine vergelijkingstrap om voor het leven te onthouden: Ruby beschouwt een BINARY-tekenreeks alleen als gelijk aan een UTF-8-tekenreeks als beide puur ASCII zijn, dus plak het nieuwe label erop voordat je gedecodeerde tekst vergelijkt met je origineel:
decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false dezelfde bytes, andere labels
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true
JWT's: een token lezen zonder de sleutel
Een JSON Web Token is drie Base64-tekenreeksen die met punten aan elkaar zijn gespijkerd: header, payload, handtekening. De eerste twee zijn URL-veilige, ongevulde Base64 van JSON-documenten, wat betekent dat een token leesbaar is voor iedereen die het ooit te zien krijgt - inclusief jou, zonder enige bibliotheek:
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"}
Voor echt werk gebruik je de jwt-gem, die het deel dat je daadwerkelijk beschermt - de handtekening - en de claim-validaties voor je afhandelt:
# In het 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
Twee beveiligingsopmerkingen horen hier, omdat beide mensen al een écht incident hebben gekost. Eerst: de payload is niet versleuteld; decoderen is lezen, niet kraken, en de handtekening is de enige bescherming, dus behandel een gedecodeerde payload nooit als vertrouwde invoer. Ten tweede: pin het algoritme in JWT.decode exact zoals getoond vast. Laat je dat achterwege, dan bepaalt de eigen header van het token hoe het geverifieerd wordt, en dat ene beetje flexibiliteit is precies wat de beroemde JWT-algoritmeverwarring-aanvallen benutten.
Basic auth: het wachtwoord verborgen voor iedereen
De oudste authenticatie-header op het web is Base64 zelf. HTTP Basic auth stuurt de aanmeldgegevens als user:password, gecodeerd, na het woord Basic - en de header reist mee met elke request, dus hij duikt op in elke log die je ooit gaat debuggen. Het decoderen ervan is strip-en-split-werk:
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!
De limiet van 2 in split maakt uit: een wachtwoord mag legaal dubbele puntjes bevatten, en je wilt alleen maar knippen bij het eerste. De eigen standaardbibliotheek van Ruby bouwt deze header in de omgekeerde richting, in Net::HTTP, en gebruikt daarvoor de core pack-template rechtstreeks:
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
En de beveiligingsopmerking die gezegd moet worden, ook al is hij voor de hand liggend: Base64 is een vertaler, geen slot. Basic auth is alleen aanvaardbaar via HTTPS. De codering bestaat zodat aanmeldgegevens de draad op kunnen als printbare tekst, niet zodat ze geheim blijven.
Data-URI's: de afbeelding die geen bestand is
Een data-URI verbergt een heel bestand binnen een URL: een media-type, het woord base64, een komma, en de gecodeerde bytes. Browsers renderen ze in img-tags en CSS, en single-file HTML-apps houden ervan, omdat er geen tweede request nodig is. Eén bouwen in Ruby kost één regel:
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
Eén decoderen is de omgekeerde richting, met twee details die mensen omver brengen. De komma is de scheiding, dus knip exact één keer, en het media-type-deel kan van alles zijn, inclusief niets:
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)
Gebruik hier strict_decode64, niet decode64: een data-URI-payload is één schone regel, en je wilt een luidruchtige fout als die beschadigd is. Houd ook de groottebelasting in gedachten - elke afbeelding die je inline zet groeit met ongeveer een derde - dus zijn data-URI's perfect voor favicons, kleine logos en fonts, en een slecht idee voor hero-afbeeldingen.
E-mail: regels van zestig tekens en de mail-gem
Base64 is uitgevonden voor e-mail, en de littekens zijn zichtbaar. SMTP is ontworpen voor korte regels van 7-bit-tekst, dus MIME Base64 wikfelt zijn uitvoer in korte regels, en een conforme decoder moet de regeleinden negeren. Rubys decode64 gedraagt zich precies zo, dus is een omwikkeld MIME-lichaam makkelijk voer:
body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"
Je zult dat zelden handmatig schrijven. De mail-gem doet het hele MIME-werk voor je: bijlagen worden automatisch Base64-gecodeerd, de regels worden omwikkeld op 60 tekens, met ruime marge binnen MIME's limiet van 76 tekens, en de juiste headers worden erbij gezet:
# In het 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
# het bijlage-deel draagt Content-Transfer-Encoding: base64
Dezelfde trick verstopt zich ook in e-mail-headers. Een niet-ASCII onderwerpregel arriveert als RFC 2047-gecodeerd woord: een tekenset, de letter B, en Base64 tussen vraagtekens. Handmatig één decoderen is een kleine oefening in tekenreekschirurgie:
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: sleutels en certificaten in pantser
Sleutels en certificaten brengen het grootste deel van hun leven door in PEM-pantsering: een BEGIN-regel, een blok Base64, en een END-regel. Het pantser dateert uit de jaren tachtig - Privacy-Enhanced Mail is waar de hele Base64-stamboom begint - maar het is nog steeds het formaat dat je .crt- en .key-bestanden vandaag dragen.
Een PEM-bestand handmatig decoderen is het pantser strippen en de tolerante decoder laten kauwen door de regeleinden heen:
require "base64"
pem = File.read("server.key")
body = pem.lines
.reject { |line| line.start_with?("-----") || line.strip.empty? }
.join
key_bytes = Base64.decode64(body)
Voor echt gebruik sla je de handmatige stap meestal over en geef je de hele PEM-tekenreeks door aan OpenSSL, die het pantser zelf leest:
require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, of wat de sleutel ook blijkt te zijn
Het enige interoperabiliteitsdetail dat het kennen waard is: PEM-regels zijn klassiek 64 tekens lang, en de decoder negeert regeleinden toch, dus een wrapper van 60 tekens of één enorme regel decodeert net zo goed.
Bestanden en de .b64-conventie
Het meest voorkomende bestandsformaat in de Base64-wereld is een gewoon tekstbestand met een .b64- (of soms .base64-)extensie dat één gecodeerde payload bevat. Een bestand lezen is een driestaps ronde reis:
require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)
Gebruik File.binwrite onderweg naar buiten - een gedecodeerde PNG of zip is binair, en schrijven in tekstmodus zou het corrumperen op platforms die regeleinden vertalen. Als je .b64-bestand van een tool kwam die regels omwikkelt, doet decode64 de regeleinden gratis af. Als je liever valideren wilt dan tolereren, lees het bestand in binair en strip de regeleinden eraf vóór een strikte decode:
encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)
Het binaire lezen telt op Windows, waar tekstmodus CRLF-regeleinden herschrijft naar LF - precies de soort mutatie die je niet wilt in een tekenreeks die je over een moment gaat valideren.
URL-veilige Base64: payloads die in links reizen
Dit is het decoderperspectief op de URL-veilige variant, want de keuze die je hier maakt bepaalt welke van de drie decoders je pakt. URL-veilige Base64 (RFC 4648, sectie 5) ruilt de twee tekens die URLs niet lusten - + wordt -, / wordt _ - en laat doorgaans de vulling ook weg. In Ruby kom je het tegen in query-parameters, cookie-waarden, API-identifiers, YouTube-stijl video-ID's en uiteraard JWT's.
Hier is hoe de drie decoders zich gedragen op dezelfde invoer, want de verschillen zitten precies waar bugs geboren worden:
| Invoer | decode64 | strict_decode64 | urlsafe_decode64 |
|---|---|---|---|
aGVsbG8= (standaard, met vulling) |
"hello" |
"hello" |
"hello" |
aGVsbG8 (zonder vulling) |
"hello" |
ArgumentError |
"hello" |
SGVsbG8gd29ybGQ- (koppelteken in de laatste groep) |
"Hello world" (één byte te kort!) |
ArgumentError |
12 bytes, het juiste antwoord |
aGVsbG8=\n (regeleinde achteraan) |
"hello" |
ArgumentError |
ArgumentError |
aGVs!bG8= (dwalend uitroepteken) |
"hello" |
ArgumentError |
ArgumentError |
Rij drie is de rij die mensen beetneemt. Een URL-veilige payload die met de standaarddecoder gedecodeerd wordt, verliest stil zijn laatste byte in plaats van iets te gooien, omdat decode64 het koppelteken simpelweg negeert. Als een payload uit een URL kan komen, decodeer hem dan met urlsafe_decode64.
Eén praktische noot: als je ooit een URL-veilige payload moet verplaatsen naar een context die alleen het standaardalfabet begrijpt (een bibliotheek, een extern systeem), is de klassieke interop-trick - vertaal het alfabet en voeg de vulling zelf toe - drie regels:
def standardize_urlsafe(b64)
b64 = b64.tr("-_", "+/")
b64 += "=" * ((4 - b64.length % 4) % 4)
b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"
Je zult het zelden nodig hebben - urlsafe_decode64 vult al voor je aan - maar het is het patroon dat je moet herkennen in de code van anderen, en het patroon waar je naar grijpt als het standaardalfabet is wat de andere kant verwacht.
Config, omgevingsvariabelen en databases
Base64 duikt op in configuratie wanneer binair data maar in een tekstdocument moet zitten. Een .env-bestand, een YAML-config of een JSON-instellingenblob kunnen ruwe bytes niet veilig dragen, dus de bytes worden gecodeerd, en ergens in je applicatie moeten ze bij het opstarten weer gedecodeerd worden:
require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)
YAML krijgt een speciale noot, want het formaat heeft een native binary-tag. Wanneer je een BINARY-tekenreeks dump, schrijft Psych hem weg als !binary-scalar met Base64 erin, en laden geeft je bytes onbeschadigd terug - helemaal geen handmatige codering nodig:
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
In databases is de vuistregel: als je database een echt binair type heeft, gebruik het dan. Base64-in-een-TEXT-kolom is het patroon waar je naar grijpt als de opslaglaag alleen strings spreekt - sommige document stores, JSON-achtige API's, of een legacy-schema dat je niet kunt veranderen - en de prijs is de groottebelasting van een derde op de kolom, plus de discipline om onderweg naar binnen te decoderen en onderweg naar buiten te hercoderen, bij elke grens.
Grote invoer, stabiel geheugen
De module is buffer-gebaseerd: een decode-aanroep leest de hele tekenreeks in één keer en geeft het volledige resultaat terug. Er zit geen streaming decoder in de standaardbibliotheek, dus het eerlijke advies voor grote payloads is: plan je geheugen. Het goede nieuws is dat decoderen alleen maar dingen kleiner maakt - de uitvoer is op zijn hoogst driekwart van de invoer - dus de invoertekenreeks is je enige grote allocatie.
Als een payload groot genoeg is om je zorgen te maken, kun je hem decoderen in groepen van vier tekens, want Base64-groepen van vier zijn zelfstandig en de laatste partiële groep draagt zijn eigen vulling:
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
Dit werkt op schone, niet-omwikkelde invoer - dezelfde regels die strict_decode64 afdwingt - want een enkele hangende groep is alleen geldig als de vulling erbij zit. Voor de écht enorme bestanden, archieven van meerdere gigabyte en dergelijke, is het patroon om het bestand in slices te lezen, elke slice te decoderen, en de bytes naar schijf te streamen, zodat er nooit meer dan één slice tegelijk in het geheugen zit.
One-liners voor de terminal
Je hebt geen scriptbestand nodig om in de shell te decoderen. Ruby kan de module ter plekke require'n:
ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world
En voor bestanden, geef dan een bestandspad door in plaats van de payload zelf:
ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin
Twee valkuilen wonen hier. Eerst: als je door echo of een ander tekstcommando pipe, reist er een regeleinde achteraan mee, en strict_decode64 gooit daarop een fout - gebruik dan decode64, of chomp de invoer:
echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'
Ten tweede: houd print in plaats van puts voor binair output, want puts voegt een regeleinde van zichzelf toe en zou het einde van je herstelde bestand corrumperen.
Valkuilen die Ruby-ontwikkelaars daadwerkelijk raken
- decode64 gooit nooit iets. Rommel in, rommel uit. Als je onbetrouwbare invoer hebt en je accepteert stil corrupte bytes, duikt de bug weken later op in een corrupt bestand, niet op de decode-regel. Ga standaard uit van een strikte decoder voor alles wat je niet zelf hebt geëcodeerd.
- strict_decode64 en het regeleinde achteraan. Tekstbestanden, echo-pipes en copy-paste houden er allemaal van om te eindigen met een regeleinde, en de strikte decoder gooit daar een
ArgumentErrorop. Eerstchompde invoer - of lees hem in binair en verwijder de regeleinden. - De coderingsstap vergeten. Een gedecodeerde tekenreeks is BINARY tot je het anders verklaart. Forceer UTF-8 (en check de geldigheid) voordat je het resultaat als tekst behandelt, anders krijg je op het moment dat je het mengt met UTF-8-tekenreeksen mojibake en
Encoding::CompatibilityError. - BINARY vergelijken met UTF-8. Dezelfde bytes, andere labels, en
==zegt false - tenzij de tekenreeks toevallig puur ASCII is. Plak het label eraan vóór je vergelijkt. - URL-veilige invoer door de verkeerde decoder. Koppeltekens en underscores worden door
decode64stilletjes weggegooid, dus een URL-veilige payload komt één byte te kort en gecorrumpeerd terug, zonder enige fout. Gebruikurlsafe_decode64. - Data na de vulling is onzichtbaar.
decode64stopt bij het eerste=. Perfect voor MIME, vreselijk voor het vangen van een payload die afgekapt is en dan door een andere tool opnieuw is gevuld. - Niet-kanonieke vulling wordt stilletjes geaccepteerd. Een tekenreeks zoals
QR==draagt pad-bits die een degelijke encoder op nul had gezet;decode64decodeert hem met plezier, terwijlstrict_decode64hem afwijst. Niets zal je ooit vertellen dat je encoder loog. - Bestandslezingen in tekstmodus op Windows schrijven regeleinden om nog vóórdat je ze ooit ziet. Lees
.b64-bestanden in binair als je ze wilt valideren.
Goede gewoontes voor de decodeerzijde
- Kies de decoder op basis van de bron van de data:
strict_decode64voor alles wat niet te vertrouwen is (en rescueArgumentErrorals de tak voor ongeldige invoer),urlsafe_decode64voor payloads die uit een URL komen, endecode64alleen voor formats die écht tolerant zijn, zoals MIME-lichamen. - Het moment dat bytes gedecodeerd zijn, bepaal dan hun identiteit: binair (houd ASCII-8BIT aan, schrijf met
File.binwrite) of tekst (force_encodingnaar UTF-8, daarnavalid_encoding?vóór gebruik). - Decodeer nooit en vertrouw niet. Een JWT-payload is leesbaar precies omdat het Base64 is; de handtekening bepaalt of het echt is. Een Base64-tekenreeks in een config-bestand is data, niet bewijs.
- Als je validators schrijft, test ze dan op de saaie gevallen: de lege tekenreeks, invoer zonder vulling, omwikkelde invoer, URL-veilige invoer, en verkeerde vulling. Dat zijn de gevallen die de drie decoders van elkaar scheiden.
Een korte geschiedenis van Base64 in Ruby
De Base64-module maakt al meer dan vijftien jaar deel uit van de standaardbibliotheek van Ruby, en de manier waarop hij wordt uitgebracht is meer veranderd dan je misschien denkt:
- 2008, Ruby 1.8.7: de module wordt geleverd met
encode64,decode64, plus twee methoden die niet meer bestaan -b64encode(omwikkeling op een gekozen regellengte) endecode_b(RFC 2047 e-mail-headerdecodering). Oude boeken en zelfs enkele oude gems refereren er nog steeds aan, en het vandaag aanroepen van een ervan is eenNoMethodError. - 2009, de 1.9-reeks:
strict_encode64,strict_decode64,urlsafe_encode64enurlsafe_decode64arriveren, en de twee legacy-methoden worden opgeheven (1.9.1 bracht beide veranderingen al mee in januari 2009). - 2015, Ruby 2.3:
urlsafe_encode64krijgt hetpadding:-keyword, zodat je ongevulde uitvoer kunt produceren voor tokens en URLs. - 2020, Ruby 3.0: base64 wordt uit de standaardbibliotheek gehaald en in zijn eigen gem gestopt, versie 0.1.0, onder de
ruby/base64-repository. Hij wordt uitgebracht als default gem, dusrequire "base64"werkt gewoon verder. - 2023, Ruby 3.3: versie 0.2.0 voegt
Base64::VERSIONtoe, plus een veel rijkere documentatiereeks. - 2024, Ruby 3.4: de gem wordt herdoopt van default gem naar bundled gem. Het praktische gevolg: in Bundler-gebaseerde projecten op Ruby 3.4 en later, zet
gem "base64"in je Gemfile (of installeer hem metgem install base64). - 2025, Ruby 4.0: versie 0.3.0 landt, onder andere met RBS-typesignaturen als onderhoud.
Door het hele verloop heen bleef één feit onveranderd: de module is een paar dozijn regels zuivere Ruby die op de core pack- en unpack-templates zitten. Geen C-extensie, geen afhankelijkheden, niets om te bouwen - en een downloadaantal in de honderden miljoenen op rubygems.org.
Ruby-weetjes voor de nieuwsgierige
- De decoderingskant van de module is twee methodelijven van één regel,
str.unpack1("m")enstr.unpack1("m0"), plus de urlsafe-variant, die een letterruil en een vullingsfix is bovenop de strikte. Je kunt de require verwijderen en het zelf schrijven. - Rubys eigen
Net::HTTPgebruikt voor Basic auth nog niet eens deBase64-module - het roept depack-template rechtstreeks aan:["user:pass"].pack("m0"). - Rails' signed en encrypted cookies zijn onder de motorkap Base64-tekenreeksen: het message codec van ActiveSupport kiest
strict_encode64voor gewone cookies enurlsafe_encode64metpadding: falsevoor URL-veilige signed IDs. Je hebt er zeker wel eens één van gedecodeerd zonder het te weten. - Elke digest-klasse heeft een
base64digest-methode -Digest::SHA256.base64digest("hello")- een one-liner voor checksums die als tekst moeten leven. - De YAML-
!binary-tag is Base64. Dump een BINARY-tekenreeks met Psych en het formaat doet de codering stilletjes voor je. decode64maakt zich er niets van of je regels 60, 64 of 76 tekens lang zijn, of één enorme regel. Dem-template slaat de regeleinden over, dus omwikkelde en niet-omwikkelde invoer decodeert identiek.
Hou door
Je hebt nu de volledige decoderingskit: een tolerante lezer voor MIME-achtige blobs, een strikte poortwachter voor alles wat niet te vertrouwen is, een URL-veilige diplomaat voor tokens en links, en de coderingsstap die de resulterende bytes omzet in tekst die Ruby je toestaat te gebruiken. De omgekeerde richting - beslissen in welke van Rubys drie encoders je je bytes voert, en het alfabet, de vulling en de regeleinden sturen - draagt zijn eigen reeks verrassingen, beginnend met een regeleinde achteraan waar niemand om vroeg. Die kant van de straat wordt in diepte behandeld in het artikel over Base64-coderen, hieronder gelinkt.
Laatst bijgewerkt: 2026-10-06
Gerelateerd artikel: Base64-codering in Ruby: een complete gids