Base64-Dekodierung in Ruby: Ein vollständiger Leitfaden
Irgendwo zwischen Ihnen und den ursprünglichen Daten steht eine Wand aus Zeichen: Groß- und Kleinbuchstaben, Ziffern, vielleicht ein Plus, ein Slash oder ein Bindestrich, und womöglich ein Gleichheitszeichen, das am Ende geparkt ist. Ihr Editor hat keine Ahnung, um welchen Dateityp es sich handelt. Ihre Datenbank hat es in eine Textspalte gepresst. Es ist in einem HTTP-Header, einer URL, einem YAML-Schlüssel oder einem Support-Ticket mit .b64-Anhang angekommen. Sie erkennen es im Nu - Base64 - und jetzt brauchen Sie die Bytes zurück. In Ruby ist dieser Bedarf genau ein require-Statement und ein Methodenaufruf entfernt.
Für den Fall, dass Ihnen das Format neu ist, hier die Dreißig-Sekunden-Version. Base64 schreibt rohe Daten drei Bytes zur Zeit um: Jede Gruppe von drei Bytes wird zu vier Zeichen aus einem 64-Symbol-Alphabet, und wenn sich die Eingabe nicht exakt durch drei teilen lässt, werden ein oder zwei =-Zeichen als Padding angehängt, damit die Ausgabe immer auf ein Vielfaches von vier landet. Dekodieren ist die Rückfahrt - vier Zeichen rein, drei Bytes raus - deshalb ist das Ergebnis immer kleiner als die Eingabe, etwa drei Viertel der Größe. Die Startseite dieser Site geht jedes Detail des Formats durch, also verbringt dieser Leitfaden seine Energie dort, wo sie hingehört: auf der Ruby-Seite des Jobs.
Die gute Nachricht: Jede Ruby-Installation liefert das komplette Dekodier-Toolkit mit. Das Base64-Modul braucht keine Installation, und seine drei Decoder sind so klein, dass Sie ihren gesamten Quellcode in einem Rutsch lesen können. Die Warnung: Der Decoder, den Sie am ehesten greifen, ist auch der, der sich nie beschwert - das ist eine schöne Eigenschaft für E-Mail und eine furchtbare für die Sicherheit. Am Ende dieses Leitfadens wissen Sie genau, was jeder Decoder akzeptiert, wie Sie die Bytes, die er zurückgibt, in Text verwandeln, den Ruby Ihnen lässt, und was Sie mit jedem Payload tun, den ein Ruby-Entwickler tatsächlich dekodiert - JWTs, Auth-Header, Data-URIs, E-Mail-Bodies, PEM-Panzerung, Dateien, Konfig-Blobs und riesige.
Die Werkzeugkiste kennenlernen
Alles beginnt mit einem require. Es gibt keinen Installationsschritt, keine Plattform-Macken, keine native Extension, die gebaut werden müsste:
require "base64"
puts Base64::VERSION
# => 0.2.0 in einem Standard-Ruby 3.3, zum Beispiel
Hier ist die gesamte Dekodier-Seite des Toolkits in einer Tabelle, in der Reihenfolge, wie oft Sie nach jeder Methode greifen werden:
| Decoder | Behandlung fremder Zeichen | Padding-Regeln | Wenn etwas nicht stimmt |
|---|---|---|---|
Base64.decode64(str) |
ignoriert alles, was nicht im Standardalphabet steht, einschließlich Zeilenumbrüche und Leerzeichen | irgendwas, selbst falsches Padding | nichts - es wirft nie etwas, es gibt einfach das zurück, was es dekodieren konnte |
Base64.strict_decode64(str) |
lehnt jedes Zeichen außerhalb des Standardalphabets ab | muss vorhanden sein und exakt richtig | wirft ArgumentError |
Base64.urlsafe_decode64(str) |
akzeptiert das URL-sichere Alphabet und das Standardalphabet, lehnt alles andere ab | optional, aber wenn vorhanden, muss es richtig sein | wirft ArgumentError |
Wenn Sie wissen möchten, was Ihre Tools unter der Haube tun, ist die gesamte Dekodier-Seite des Moduls nur eine dünne Hülle um zwei Vorlagen der Kern-pack/unpack-Maschinerie, die in C im Ruby-Kern implementiert ist:
# die gesamte Dekodier-Seite des Moduls, kondensiert
def decode64(str)
str.unpack1("m")
end
def strict_decode64(str)
str.unpack1("m0")
end
Die m-Vorlage ist der nachsichtige Leser, m0 der strenge, und dieser eine Buchstabenunterschied erklärt die ganze Persönlichkeitslücke zwischen den ersten beiden Decodern. Weil die harte Arbeit mit Kerngeschwindigkeit passiert, bleibt das Modul reines Ruby und verschlingt trotzdem Megabytes in einstelligen Millisekunden.
decode64: Das Chamäleon
Base64.decode64 ist der Decoder, der allem Ja sagt. Füttern Sie ihm einen sauberen Payload, und er dekodiert ihn. Füttern Sie ihm einen MIME-artigen Blob voller Zeilenumbrüche, und er zuckt die Schultern. Füttern Sie ihm einen String, der gar kein Base64 ist, und er gibt zurück, was er herausquetschen konnte, ohne eine einzige Warnung:
require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"
Die zweite Zeile ist die gesamte Persönlichkeit in einem einzigen Beispiel. Der Decoder überspringt alles, was nicht zum Standardalphabet gehört - Zeilenumbrüche, Leerzeichen, das eine oder andere Steuerzeichen - und dekodiert den Rest. Genau dieses Verhalten soll MIME-Base64 haben, deshalb ist decode64 das richtige Tool für alles, was durch E-Mail gereist ist.
Die Kehrseite ist es, was ihn gefährlich macht. Weil der Decoder sich nie beschwert, sagt er Ihnen auch nie, wann die Eingabe falsch war:
Base64.decode64("not base64 at all!")
# => zehn Bytes völlig plausibel wirkender Müll
Base64.decode64("====")
# => ""
Das erste Beispiel findet die Zeichen, die zufällig gültige Alphabetbuchstaben sind, dekodiert sie, und gibt Bytes zurück, die Sie in Versuchung bringen könnten, direkt in eine Datei zu schreiben. Das zweite Beispiel liefert für einen String aus vier Padding-Zeichen einen leeren String zurück. Nichts wird geworfen, nichts wird geloggt. Wenn Ihre Eingabe nicht vertrauenswürdig ist, ist diese Stille eine Eigenschaft, die Sie ausschalten wollen - wofür die nächsten zwei Decoder da sind.
Eine weitere Eigenart, die sich zu wissen lohnt, weil es genau die Sorte Sache ist, die sich monatelang in der Produktion versteckt: Das Dekodieren hört beim ersten =-Zeichen auf. Alles nach dem Padding ist kein Fehler, es wird schlicht nie gelesen:
Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello" der "Zm9vYmFy"-Teil ist dem Decoder unsichtbar
strict_decode64: Der Türsteher
Base64.strict_decode64 ist der Decoder mit Klemmbrett. Er akzeptiert nur das Standardalphabet (A bis Z, a bis z, 0 bis 9, Plus, Slash), verlangt, dass jedes Padding exakt richtig ist, und verweigert es, auch nur ein Byte zu produzieren, wenn auch nur eine Regel gebrochen wird:
Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => wirft ArgumentError
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => wirft ArgumentError
Die letzte Zeile ist die aufschlussreiche: derselbe Payload, den decode64 fröhlich dekodiert hat, wirft jetzt wegen eines einzelnen Zeilenumbruchs. Fehlendes Padding, überflüssiges Padding, ein Bindestrich, ein Unterstrich, ein Leerzeichen - alles davon ist ein Verbrechen, und der gesamte Payload geht mit unter:
begin
Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
puts e.message
end
# => invalid base64
Der Türsteher überwacht sogar Ecken des Formats, an die Sie nicht denken würden. Wenn ein Base64-String mit Padding endet, werden einige Bits im letzten Zeichen nie verwendet, und der RFC sagt, dass ein konformer Encoder diese Bits auf null setzen muss. Ruby prüft das:
Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => wirft ArgumentError (die Pad-Bits sind nicht null)
Der zweite String würde zum selben Byte dekodieren wie der erste, wenn der Decoder schlampig wäre. Ruby ist nicht schlampig. In der Praxis macht das strict_decode64 zum richtigen Standard für jede Eingabe, die Sie nicht selbst kodiert haben: Es verwandelt Tippfehler, Abschneiden und das falsche Alphabet in laute, abfängbare Fehler, statt in stille Korruption.
urlsafe_decode64: Der Diplomat
Base64.urlsafe_decode64 existiert für Payloads, die Orte bereisen, an denen + und / Reservewörter sind: URLs, Tokens, Datenbank-Identifikatoren. Intern übersetzt es das URL-sichere Alphabet (Bindestrich und Unterstrich) zurück ins Standardalphabet, normalisiert das Padding und übergibt das Ergebnis dem strengen Decoder:
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => wirft ArgumentError (fünfzehn Zeichen brauchen ein Pad-Zeichen, nicht zwei)
Das erste Beispiel zeigt seine nützlichste Eigenschaft: Eingabe ohne Padding ist in Ordnung. Wenn der String kein Padding hat und seine Länge kein Vielfaches von vier ist, ergänzt der Decoder die fehlenden =-Zeichen für Sie - genau das, was JSON Web Tokens, der größte Verbraucher von URL-sicherem Base64, produzieren. Wenn aber Padding vorhanden ist, muss es richtig sein, genau wie beim strengen Decoder.
Es gibt eine Eigenart, über die die Dokumentation nicht laut ruft: Der Diplomat spricht beide Sprachen. Weil die Methode Bindestriche und Unterstriche vor dem strengen Dekodieren umschreibt, akzeptiert sie auch Strings aus dem Standardalphabet:
Base64.urlsafe_decode64("aGVsbG8=")
# => "hello" das Standardalphabet wird ebenfalls akzeptiert
Diese Nachsicht ist praktisch, aber es bedeutet, dass Sie diese Methode nicht verwenden können, um zu erkennen, aus welchem Alphabet ein Payload stammt. Wenn Ihnen das wichtig ist, untersuchen Sie die Zeichen selbst, bevor Sie dekodieren.
Und anders als decode64 hat der Diplomat keine Gnade für Leerzeichen. Ein Zeilenumbruch irgendwo in einem URL-sicheren Payload wirft ArgumentError, also streichen Sie die Zeilenumbrüche zuerst, wenn Ihre Eingabe aus einer umgebrochenen Datei kommt.
Bytes sind kein Text: Der Kodierungsschritt
Hier ist der Schritt, über den selbst erfahrene Entwickler stolpern, weil Ruby ihn sichtbar macht. Ein dekodierter Base64-String ist immer mit der ASCII-8BIT-Kodierung getaggt (auch BINARY genannt), egal ob die ursprünglichen Daten ein PNG, ein JWT-Payload oder ein Liebesbrief in UTF-8 waren:
bin = Base64.decode64(Base64.strict_encode64("h\u{e9}llo"))
puts bin.encoding
# => ASCII-8BIT
puts bin.bytes
# => [104, 195, 169, 108, 108, 111]
Wenn der Payload binär ist - ein Bild, eine Zip-Datei, ein Hash - lassen Sie ihn genau so, wie er ist, und schreiben Sie ihn mit File.binwrite. Keine Umwandlung, keine Fragen. Wenn der Payload Text ist, sind die Bytes mit hoher Wahrscheinlichkeit UTF-8, und Sie müssen Ruby das mitteilen:
text = Base64.decode64(payload)
text.force_encoding("UTF-8")
if text.valid_encoding?
puts text
else
puts "not valid UTF-8 after all"
end
Die zwei Aufrufe erledigen verschiedene Jobs. force_encoding beschriftet nur die Bytes neu, und valid_encoding? überprüft dann, ob sie echtes UTF-8 bilden. Führen Sie sie in genau dieser Reihenfolge aus, weil die Überprüfung eines BINARY-Strings zuerst nichts zu überprüfen hätte. Und eine kleine Vergleichsfalle, die man fürs Leben merkt: Ruby hält einen BINARY-String nur dann für gleich mit einem UTF-8-String, wenn beide reines ASCII sind, also beschriften Sie neu, bevor Sie dekodierten Text mit Ihrem Original vergleichen:
decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false gleiche Bytes, unterschiedliche Tags
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true
JWTs: Einen Token ohne Schlüssel lesen
Ein JSON Web Token ist drei Base64-Strings, die mit Punkten zusammengeheftet sind: Header, Payload, Signatur. Die ersten beiden sind URL-sicheres, ungepaddetes Base64 von JSON-Dokumenten, was bedeutet, dass ein Token von jedem lesbar ist, der ihn je zu sehen bekommt - einschließlich Ihnen, ganz ohne Bibliothek:
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"}
Für echte Arbeit werden Sie das jwt-Gem verwenden, das den Teil übernimmt, der Sie tatsächlich schützt - die Signatur - sowie die Claim-Prüfungen:
# Im 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
Zwei Sicherheitshinweise gehören hierher, weil beide schon echten Vorfällen den Boden bereitet haben. Erstens ist der Payload nicht verschlüsselt; ihn zu dekodieren ist Lesen, nicht Knacken, und die Signatur ist der einzige Schutz, also behandeln Sie einen dekodierten Payload nie als vertrauenswürdige Eingabe. Zweitens nageln Sie den Algorithmus in JWT.decode genau so fest wie gezeigt. Lassen Sie ihn weg, und der eigene Header des Tokens entscheidet, wie es verifiziert wird - und genau diese Prise Flexibilität nutzen die berühmten JWT-Algorithmusverwechslungs-Angriffe aus.
Basic Auth: Das Passwort vor aller Augen versteckt
Der älteste Authentifizierungs-Header im Web ist Base64 selbst. HTTP Basic Auth schickt die Zugangsdaten als user:password, kodiert, hinter dem Wort Basic - und der Header reist mit jeder Anfrage mit, also taucht er in jedem Log auf, das Sie je debuggen. Einen solchen Header zu dekodieren ist eine Sache von Streichen und Trennen:
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!
Die Grenze 2 in split ist wichtig: Ein Passwort darf legal Doppelpunkte enthalten, und Sie wollen immer nur am ersten trennen. Die eigene Standardbibliothek von Ruby baut diesen Header umgekehrt auf, in Net::HTTP, und verwendet dafür die Kern-pack-Vorlage direkt:
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
Und der Sicherheitshinweis, der gesagt werden muss, auch wenn er offensichtlich ist: Base64 ist ein Übersetzer, kein Schloss. Basic Auth ist nur über HTTPS akzeptabel. Die Kodierung existiert dafür, dass Zugangsdaten als druckbarer Text die Leitung entlang reisen können, nicht dafür, dass sie geheim sind.
Data-URIs: Das Bild, das keine Datei ist
Ein Data-URI versteckt eine ganze Datei in einer URL: ein Medientyp, das Wort base64, ein Komma und die kodierten Bytes. Browser rendern sie in img-Tags und CSS, und Single-File-HTML-Apps lieben sie, weil keine zweite Anfrage nötig ist. Einen in Ruby zu bauen dauert eine Zeile:
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
Dekodieren ist die Umkehrung, mit zwei Details, über die Leute stolpern. Das Komma ist der Trenner, also genau einmal splitten, und der Medientyp-Teil kann alles sein, einschließlich nichts:
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)
Verwenden Sie hier strict_decode64, nicht decode64: Der Payload eines Data-URIs ist eine einzige saubere Zeile, und Sie wollen einen lauten Fehler, falls er beschädigt ist. Bedenken Sie auch die Größensteuer - jedes inline eingebettete Bild wächst um etwa ein Drittel - deshalb sind Data-URIs perfekt für Favicons, kleine Logos und Schriftarten, aber eine schlechte Idee für Hero-Fotos.
E-Mail: Zeilen mit 60 Zeichen und das mail-Gem
Base64 wurde für E-Mail erfunden, und die Narben sind sichtbar. SMTP wurde für kurze Zeilen aus 7-Bit-Text entworfen, deshalb bricht MIME-Base64 seine Ausgabe in kurze Zeilen um, und ein konformer Decoder muss die Zeilenumbrüche ignorieren. Rubys decode64 verhält sich genau so, also sind umgebrochene MIME-Bodies leichte Beute:
body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"
Selten schreiben Sie das von Hand. Das mail-Gem erledigt die gesamte MIME-Arbeit für Sie: Anhänge werden automatisch Base64-kodiert, die Zeilen werden bei 60 Zeichen umgebrochen, deutlich innerhalb von MIMEs 76-Zeichen-Limit, und die richtigen Header werden angehängt:
# Im 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
# der Anhang trägt Content-Transfer-Encoding: base64
Derselbe Trick versteckt sich auch in E-Mail-Headern. Betreffzeilen, die nicht ASCII sind, kommen als RFC-2047-kodierte Wörter an: ein Zeichensatz, der Buchstabe B und Base64 zwischen Fragezeichen. Von Hand eines zu dekodieren ist eine kleine Übung in String-Chirurgie:
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: Schlüssel und Zertifikate in Panzerung
Schlüssel und Zertifikate verbringen das meiste ihres Lebens in PEM-Panzerung: eine BEGIN-Zeile, ein Block aus Base64 und eine END-Zeile. Die Panzerung stammt aus den 1980er-Jahren - Privacy-Enhanced Mail ist der Ursprung der ganzen Base64-Linie - aber sie ist immer noch das Format, das Ihre .crt- und .key-Dateien heute tragen.
Eine PEM-Datei von Hand zu dekodieren ist nur das Streifen der Panzerung und dem nachsichtigen Decoder die Zeilenumbrüche durchkauen zu lassen:
require "base64"
pem = File.read("server.key")
body = pem.lines
.reject { |line| line.start_with?("-----") || line.strip.empty? }
.join
key_bytes = Base64.decode64(body)
Für den eigentlichen Gebrauch werden Sie den manuellen Schritt meistens überspringen und den gesamten PEM-String an OpenSSL übergeben, der die Panzerung selbst liest:
require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA, oder was auch immer der Schlüssel am Ende ist
Das einzige Interop-Detail, das es zu wissen lohnt: PEM-Zeilen sind klassisch 64 Zeichen lang, und der Decoder ignoriert Zeilenumbrüche ohnehin, also dekodiert ein 60-Zeichen-Umbruch oder eine einzige riesige Zeile genauso gut.
Dateien und die .b64-Konvention
Das häufigste Dateiformat in der Base64-Welt ist eine gewöhnliche Textdatei mit einer .b64 (manchmal .base64)-Endung, die einen einzigen kodierten Payload enthält. Eine zu lesen sind drei Schritte:
require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)
Verwenden Sie beim Hinausgehen File.binwrite - ein dekodiertes PNG oder eine Zip-Datei ist binär, und das Schreiben im Text-Modus würde es auf Plattformen, die Zeilenendungen übersetzen, beschädigen. Wenn Ihre .b64-Datei von einem Tool kommt, das Zeilen umbricht, erledigt decode64 die Zeilenumbrüche gratis. Wenn Sie lieber validieren als tolerieren, lesen Sie die Datei im Binärmodus und streichen Sie die Zeilenumbrüche vor einem strengen Dekodieren:
encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)
Das binäre Lesen ist auf Windows wichtig, wo der Text-Modus CRLF-Zeilenendungen als LF umschreibt - genau die Art Mutation, die Sie in einem String nicht geschehen sehen wollen, den Sie gleich zu validieren gedenken.
URL-sicheres Base64: Payloads, die in Links reisen
Das ist der Decoder-Blick auf die URL-sichere Variante, weil die Wahl, die Sie hier treffen, verändert, nach welchem der drei Decoder Sie greifen. URL-sicheres Base64 (RFC 4648, Abschnitt 5) tauscht die zwei Zeichen aus, die URLs nicht mögen - + wird zu -, / wird zu _ - und lässt meistens auch das Padding weg. In Ruby treffen Sie es in Query-Parametern, Cookie-Werten, API-Identifikatoren, YouTube-artigen Video-IDs und natürlich in JWTs.
Hier ist, wie die drei Decoder auf denselben Eingaben reagieren, denn genau dort, wo die Unterschiede liegen, entstehen die Bugs:
| Eingabe | decode64 | strict_decode64 | urlsafe_decode64 |
|---|---|---|---|
aGVsbG8= (standard, mit Padding) |
"hello" |
"hello" |
"hello" |
aGVsbG8 (ohne Padding) |
"hello" |
ArgumentError |
"hello" |
SGVsbG8gd29ybGQ- (Bindestrich in der letzten Gruppe) |
"Hello world" (ein Byte kürzer!) |
ArgumentError |
12 Bytes, die richtige Antwort |
aGVsbG8=\n (anhängender Zeilenumbruch) |
"hello" |
ArgumentError |
ArgumentError |
aGVs!bG8= (verirrtes Ausrufezeichen) |
"hello" |
ArgumentError |
ArgumentError |
Die dritte Zeile ist die, die Leute beißt. Ein URL-sicherer Payload, dekodiert mit dem Standard-Decoder, verliert still sein letztes Byte, anstatt etwas zu werfen, weil decode64 den Bindestrich einfach ignoriert. Kann ein Payload aus einer URL kommen, dekodieren Sie ihn mit urlsafe_decode64.
Ein praktischer Hinweis: Wenn Sie je einen URL-sicheren Payload in einen Kontext verschieben müssen, der nur das Standardalphabet versteht (eine Bibliothek, ein fremdes System), ist der klassische Interop-Trick - das Alphabet übersetzen und das Padding selbst ergänzen - drei Zeilen:
def standardize_urlsafe(b64)
b64 = b64.tr("-_", "+/")
b64 += "=" * ((4 - b64.length % 4) % 4)
b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"
Sie werden es selten brauchen - urlsafe_decode64 füllt bereits für Sie auf - aber es ist das Muster, das Sie im Code anderer Leute erkennen sollten, und das Muster, nach dem Sie greifen, wenn das Standardalphabet ist, was die andere Seite erwartet.
Konfiguration, Umgebungsvariablen und Datenbanken
Base64 taucht in der Konfiguration immer dann auf, wenn Binärdaten in einem Textdokument sitzen müssen. Eine .env-Datei, eine YAML-Konfiguration oder ein JSON-Einstellungen-Blob können rohe Bytes nicht sicher tragen, also werden die Bytes kodiert, und irgendetwas in Ihrer Anwendung muss sie beim Start dekodieren:
require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)
YAML verdient eine gesonderte Erwähnung, weil das Format einen nativen Binär-Tag hat. Wenn Sie einen BINARY-String dumpen, schreibt Psych ihn als !binary-Skalar mit Base64 heraus, und das Laden gibt Ihre Bytes intakt zurück - ganz ohne manuelle Kodierung:
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 Datenbanken gilt die Faustregel: Wenn Ihre Datenbank einen echten Binär-Typ hat, verwenden Sie ihn. Base64-in-einer-TEXT-Spalte ist das Muster, nach dem Sie greifen, wenn die Schicht darunter nur Strings spricht - manche Dokumenten-Store, JSON-artige APIs oder ein Legacy-Schema, das Sie nicht ändern können - und der Preis ist die ein-Drittel-Größensteuer auf der Spalte, plus die Disziplin, beim Hineinkommen zu dekodieren und beim Hinauskommen neu zu kodieren, an jeder Grenze.
Große Eingaben, ruhiger Speicher
Das Modul ist pufferbasiert: Ein Dekodieraufruf liest den gesamten String auf einmal und gibt das gesamte Ergebnis zurück. Es gibt keinen Streaming-Decoder in der Standardbibliothek, also lautet der ehrliche Rat für große Payloads: den Speicher planen. Die gute Nachricht ist, dass Dekodieren Dinge nie größer macht - die Ausgabe ist höchstens drei Viertel der Eingabe - deshalb ist der Eingabe-String Ihre einzige große Allokation.
Wenn ein Payload groß genug ist, dass es Sie beunruhigt, können Sie ihn in Vier-Zeichen-Gruppen dekodieren, denn Base64-Gruppen von vier sind in sich abgeschlossen und die letzte unvollständige Gruppe trägt ihr eigenes 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
Das funktioniert auf sauberer, nicht umgebrochener Eingabe - denselben Regeln, die strict_decode64 durchsetzt - denn eine einzelne anhängende Gruppe ist nur mit ihrem Padding gültig. Für die wirklich riesigen Dateien, Multi-Gigabyte-Archive und dergleichen, ist das Muster, die Datei in Scheiben zu lesen, jede Scheibe zu dekodieren und die Bytes auf die Festplatte zu streamen, sodass nie mehr als eine Scheibe im Speicher ist.
Einzeiler für das Terminal
Um in der Shell etwas zu dekodieren, brauchen Sie keine Skriptdatei. Ruby kann das Modul auf der Stelle requiren:
ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world
Und für Dateien übergeben Sie einen Dateipfad statt des Payloads selbst:
ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin
Zwei Stolperfallen wohnen hier. Erstens: Wenn Sie durch echo oder ein anderes Text-Kommando pipen, reist ein anhängender Zeilenumbruch mit, und strict_decode64 wirft darauf - verwenden Sie decode64, oder chompen Sie die Eingabe:
echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'
Zweitens: Behalten Sie für binäre Ausgabe print bei, statt puts zu nehmen, denn puts hängt einen eigenen Zeilenumbruch an und würde das Ende Ihrer wiederhergestellten Datei beschädigen.
Fallen, die Ruby-Entwickler tatsächlich treffen
- decode64 wirft nie. Müll rein, Müll raus. Wenn Ihre Eingabe nicht vertrauenswürdig ist und Sie beschädigte Bytes still akzeptieren, wird der Bug Wochen später in einer korrupten Datei auftauchen, nicht auf der Decode-Zeile. Gehen Sie für alles, was Sie nicht selbst kodiert haben, standardmäßig zu einem strengen Decoder.
- strict_decode64 und der anhängende Zeilenumbruch. Textdateien, echo-Pipes und Copy-Paste lieben es, mit einem Newline zu enden, und der strenge Decoder wirft darauf
ArgumentError.chompen Sie die Eingabe zuerst - oder lesen Sie sie im Binärmodus und löschen Sie die Zeilenumbrüche. - Den Kodierungsschritt vergessen. Ein dekodierter String ist BINARY, bis Sie etwas anderes sagen. Erzwingen Sie UTF-8 (und prüfen Sie die Gültigkeit), bevor Sie das Ergebnis als Text behandeln, sonst bekommen Sie Mojibake und
Encoding::CompatibilityErrorin dem Moment, in dem Sie es mit UTF-8-Strings vermischen. - BINARY mit UTF-8 vergleichen. Gleiche Bytes, unterschiedliche Tags, und
==sagt false - es sei denn, der String ist zufällig reines ASCII. Beschriften Sie neu, bevor Sie vergleichen. - URL-sichere Eingabe durch den falschen Decoder. Bindestriche und Unterstriche werden von
decode64still fallen gelassen, also kommt ein URL-sicherer Payload ein Byte kürzer und beschädigt zurück, ohne jeglichen Fehler. Verwenden Sieurlsafe_decode64. - Daten nach dem Padding sind unsichtbar.
decode64hört beim ersten=auf. Fantastisch für MIME, furchtbar dafür, einen Payload zu erwischen, der abgeschnitten und dann von einem anderen Tool neu gepaddet wurde. - Non-kanonisches Padding wird still akzeptiert. Ein String wie
QR==trägt Pad-Bits, die ein ordentlicher Encoder auf null gesetzt hätte;decode64dekodiert ihn fröhlich, währendstrict_decode64ihn ablehnt. Nichts wird Sie je darauf bringen, dass Ihr Encoder gelogen hat. - Datei-Lesezugriffe im Text-Modus auf Windows schreiben Zeilenendungen um, bevor Sie sie je zu Gesicht bekommen. Lesen Sie
.b64-Dateien im Binärmodus, wenn Sie sie validieren wollen.
Gute Gewohnheiten für die Decode-Seite
- Wählen Sie den Decoder aus der Herkunft der Daten:
strict_decode64für alles Unvertrauenswürdige (und rescuen SieArgumentErrorals Ihren ungültigen-Eingabe-Zweig),urlsafe_decode64für URL-geborene Payloads,decode64nur für Formate, die genuinely nachsichtig sind, wie MIME-Bodies. - Im Moment, in dem Bytes dekodiert sind, entscheiden Sie ihre Identität: binär (ASCII-8BIT behalten, mit
File.binwriteschreiben) oder Text (force_encodingauf UTF-8, dannvalid_encoding?vor der Verwendung). - Dekodieren Sie nie und vertrauen Sie nicht. Ein JWT-Payload ist lesbar, gerade weil er Base64 ist; die Signatur entscheidet, ob er echt ist. Ein Base64-String in einer Konfigurationsdatei ist Daten, nicht Beweis.
- Wenn Sie Validatoren schreiben, testen Sie sie gegen die langweiligen Fälle: den leeren String, Eingabe ohne Padding, umgebrochene Eingabe, URL-sichere Eingabe und falsches Padding. Das sind die Fälle, die die drei Decoder voneinander trennen.
Eine kurze Geschichte von Base64 in Ruby
Das Base64-Modul ist seit über fünfzehn Jahren Teil der Standardbibliothek von Ruby, und die Art, wie es ausgeliefert wird, hat sich öfter geändert, als Sie vielleicht denken:
- 2008, Ruby 1.8.7: Das Modul erscheint mit
encode64unddecode64plus zwei Methoden, die nicht mehr existieren -b64encode(Umbruch bei einer gewählten Zeilenlänge) unddecode_b(RFC-2047-E-Mail-Header-Dekodierung). Alte Bücher und sogar manche alten Gems verweisen noch auf sie, und ein Aufruf von jedem der beiden ist heute einNoMethodError. - 2009, die 1.9-Linie:
strict_encode64,strict_decode64,urlsafe_encode64undurlsafe_decode64treffen ein, und die beiden Legacy-Methoden werden in den Ruhestand versetzt (1.9.1 hatte beide Änderungen bereits im Januar 2009 ausgeliefert). - 2015, Ruby 2.3:
urlsafe_encode64bekommt daspadding:-Keyword und lässt Sie damit ungepaddete Ausgabe für Tokens und URLs erzeugen. - 2020, Ruby 3.0: base64 wird aus der Standardbibliothek in ein eigenes Gem extrahiert, Version 0.1.0, unter dem
ruby/base64-Repository. Es erscheint als Default-Gem, also funktioniertrequire "base64"einfach weiter. - 2023, Ruby 3.3: Version 0.2.0 fügt
Base64::VERSIONund einen deutlich reicheren Dokumentations-Satz hinzu. - 2024, Ruby 3.4: Das Gem wird von einem Default-Gem zu einem Bundled-Gem umklassifiziert. Die praktische Konsequenz: In Bundler-basierten Projekten auf Ruby 3.4 und neuer listen Sie
gem "base64"in Ihrem Gemfile (oder installieren Sie es mitgem install base64). - 2025, Ruby 4.0: Version 0.3.0 landet und bringt RBS-Typsignaturen mit, unter anderem weitere Wartungsarbeiten.
Durch all das hindurch hat sich eine Tatsache nie geändert: Das Modul ist ein paar Dutzend Zeilen reines Ruby auf den Kern-pack- und unpack-Vorlagen. Keine C-Extension, keine Abhängigkeiten, nichts zu bauen - und eine Download-Zahl im Bereich von Hunderten Millionen auf rubygems.org.
Ruby-Trivia für Neugierige
- Die Dekodier-Seite des Moduls sind zwei einzeilige Methodenkörper,
str.unpack1("m")undstr.unpack1("m0"), plus die urlsafe-Variante, die ein Buchstabentausch und ein Padding-Fix auf dem strengen Decoder sind. Sie können das require löschen und es selbst schreiben. - Das eigene
Net::HTTPvon Ruby verwendet nicht einmal dasBase64-Modul für Basic Auth - es ruft diepack-Vorlage direkt auf:["user:pass"].pack("m0"). - Die signierten und verschlüsselten Cookies von Rails sind unter der Haube Base64-Strings: Der Message-Codec von ActiveSupport wählt
strict_encode64für gewöhnliche Cookies undurlsafe_encode64mitpadding: falsefür URL-sichere signierte IDs. Wahrscheinlich haben Sie eines davon schon dekodiert, ohne es zu wissen. - Jede Digest-Klasse hat eine
base64digest-Methode -Digest::SHA256.base64digest("hello")- einen Einzeiler für Prüfsummen, die in Text leben müssen. - Der
!binary-Tag von YAML ist Base64. Dumpen Sie einen BINARY-String mit Psych, und das Format erledigt die Kodierung still für Sie. decode64schert sich nicht darum, ob Ihre Zeilen 60, 64 oder 76 Zeichen lang sind oder eine einzige riesige Zeile. Diem-Vorlage überspringt die Zeilenumbrüche, also dekodieren umgebrochene und nicht umgebrochene Eingaben identisch.
Weiter geht's
Sie haben jetzt das komplette Dekodier-Toolkit: einen nachsichtigen Leser für MIME-förmige Blobs, einen strengen Türsteher für alles Unvertrauenswürdige, einen URL-sicheren Diplomaten für Tokens und Links und den Kodierungsschritt, der die resultierenden Bytes in Text verwandelt, den Ruby Ihnen lässt. Die umgekehrte Richtung - zu entscheiden, welchen der drei Encoder von Ruby Sie mit Ihren Bytes füttern, und das Alphabet, das Padding und die Zeilenumbrüche zu steuern - hat ihren eigenen Satz Überraschungen, beginnend mit einem anhängenden Newline, den niemand bestellt hat. Diese andere Seite der Straße wird in dem Artikel über Base64-Kodierung in Ruby, verlinkt unten, in derselben Tiefe abgedeckt.
Zuletzt aktualisiert: 2026-09-08
Verwandter Artikel: Base64-Kodierung in Ruby: Ein vollständiger Leitfaden