Ruby での Base64 デコード:完全ガイド
あなたと元のデータのあいだのどこかに、文字の壁が立ちはだかっています。大文字と小文字、数字、たまにプラスかスラッシュかハイフン、そして末尾にイコール記号が止まっていることもあります。エディターはその文字列がどんなファイル形式なのか、見当もつきません。データベースはそれをテキスト列に押し込んでしまった。HTTP ヘッダー、URL、YAML キー、あるいは .b64 ファイルを添付したサポートチケットの中で、それがあなたの手元へやってきた。あなたは即座に見抜きます - Base64 - そして今、バイトを取り戻す必要に迫られています。Ruby なら、その必要は require 文 1 つとメソッド呼び出し 1 回で叶います。
この形式が初めての方は、30 秒版から行きましょう。Base64 は生データを 3 バイトずつ書き換えます。3 バイトの 1 グループが 64 文字のアルファベットから 4 文字に変わり、入力が 3 で割り切れないときは、出力が必ず 4 の倍数に収まるよう、1 つか 2 つの = がパディングとして末尾に付けられます。デコードはその逆の行程です - 4 文字が入って 3 バイトが出る - だから結果は常に、おおよそ入力の 3 分の 4 という、入力より小さいものになります。このサイトのホームページではこの形式の細部まで丸ごと丁寧に扱っているので、このガイドが力を注ぐのは本来の場所、Ruby 側の仕事のほうです。
朗報から行きましょう。どの Ruby インストールにも、デコードのツール一式が最初から入っています。Base64 モジュールには何もインストールする必要がなく、3 つのデコーダーはどれも小さく、ソース全部を 1 回の読みで読めてしまいます。ただし注意も必要です。一番最初に手を出すことになるデコーダーは、文句を言わないデコーダーでもある - これはメールには美しい性質で、セキュリティには最悪の性質です。このガイドの終わりまでに、各デコーダーが何を受理するのか、返ってきたバイトを Ruby が使えるテキストにする方法を、そして Ruby 開発者が実際にデコードするあらゆるペイロード - JWT、認証ヘッダー、data URI、メール本文、PEM 鎧、ファイル、設定の塊、そして巨大なもの - にどう対処するか、正確にわかるようになるでしょう。
ツールボックスの紹介
始まりは require 1 行です。インストールの手順もなく、プラットフォーム特有の癖もなく、ビルドするネイティブ拡張もありません:
require "base64"
puts Base64::VERSION
# => 素の Ruby 3.3 なら 0.2.0、例えば
ツールボックスのデコード側全部を 1 つの表にまとめました。各メソッドに手を出す頻度の高い順です:
| デコーダー | アルファベット外文字の扱い | パディングのルール | 何か違うとき |
|---|---|---|---|
Base64.decode64(str) |
改行や空白を含め、標準アルファベットにないものはすべて無視する | 何でも通す。誤ったパディングでも | 何もしない - 決して送出せず、デコードできた分を返すだけ |
Base64.strict_decode64(str) |
標準アルファベット外の文字はすべて拒否する | 存在して、かつ完全に正しくなければならない | ArgumentError を送出 |
Base64.urlsafe_decode64(str) |
URL 安全なアルファベットと標準アルファベットを受理し、それ以外は拒否する | 任意。ただし存在する場合は正しくなければならない | ArgumentError を送出 |
ツールの中身がどういうことになっているのか気になる方には、このモジュールのデコード側全体は、Ruby コアの C 実装である pack/unpack という機構の 2 つのテンプレートへの薄いラッパーにすぎません:
# このモジュールのデコード側全体を、短縮した形
def decode64(str)
str.unpack1("m")
end
def strict_decode64(str)
str.unpack1("m0")
end
m テンプレートが寛容な読み手で、m0 が厳格な読み手です。この 1 文字の違いが、最初の 2 つのデコーダーの気質の差を丸ごと説明しています。重労働はコアの速度でこなされるので、このモジュールは純粋な Ruby のまま、MB 単位の入力を 1 桁ミリ秒でかじり切ります。
decode64:カメレオン
Base64.decode64 は、何にでもイエスと言うデコーダーです。きれいなペイロードを渡せばデコードします。改行だらけの MIME 風の塊を渡せば、ただ肩をすくめて済ませます。Base64 ではない文字列を渡しても、絞め出せた分を何も警告せずに返してきます:
require "base64"
Base64.decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.decode64("Zm9vCmJh\ncgptYW4=\n")
# => "foo\nbar\nman"
2 行目がこのデコーダーの人格全部を表しています。デコーダーは標準アルファベットの一部でないもの - 改行、空白、たまに現れる制御文字 - を全部スキップして、残りをデコードします。MIME Base64 が本来そうあるべき姿とまさに同じ振る舞いで、だからこそ decode64 は、メールの中を旅してきたものすべてにとって正しいツールなのです。
表裏一体で、それが危険な部分でもあります。デコーダーが文句を言わない以上、入力が間違っていたときも教えてもらえません:
Base64.decode64("not base64 at all!")
# => 見た目だけはまったくもっともらしい 10 バイトのゴミ
Base64.decode64("====")
# => ""
1 番目の例では、たまたま有効なアルファベットの文字である部分だけを見つけ出してデコードし、そのままファイルに書き込みたくなるようなバイトを返してきます。2 番目の例では、パディング文字 4 つの文字列に対して空の文字列を返します。何も送出され、何も記録されません。入力に信頼性がなければ、その沈黙はオフにしたい機能です - それが、次の 2 つのデコーダーが存在する理由です。
もう 1 つ、知っておく価値のある癖があります。本番環境で何ヶ月も潜伏するタイプのものです:デコードは最初の = 文字で止まります。パディングより後のものはエラーにならず、ただ読まれないだけです:
Base64.decode64("aGVsbG8=Zm9vYmFy")
# => "hello" 「Zm9vYmFy」の部分はデコーダーには見えない
strict_decode64:番人
Base64.strict_decode64 は、チェックリストを持ったデコーダーです。受理するのは標準アルファベット(A-Z、a-z、0-9、プラス、スラッシュ)のみ。パディングは完全な正しさを要求し、ルールが一つでも破られていれば、1 バイトすら出しません:
Base64.strict_decode64("aGVsbG8gd29ybGQ=")
# => "hello world"
Base64.strict_decode64("aGVsbG8gd29ybGQ")
# => ArgumentError を送出
Base64.strict_decode64("Zm9vCmJh\ncgptYW4=")
# => ArgumentError を送出
物語っているのは最後の行です:decode64 が喜んでデコードした同じペイロードが、改行 1 つのために送出します。パディング不足、パディング余り、ハイフン、アンダースコア、空白 - いずれも犯罪で、ペイロード全体がその罪に巻き込まれます:
begin
Base64.strict_decode64("aGVsbG8")
rescue ArgumentError => e
puts e.message
end
# => invalid base64
番人は、あなたが確認しようとすら思わない形式の隅までパトロールします。Base64 文字列がパディングで終わるとき、最後の文字のビットの一部は使われませんが、RFC は、準拠するエンコーダーはそれらのビットをゼロにしなければならないと定めています。Ruby は検証します:
Base64.strict_decode64("QQ==")
# => "A"
Base64.strict_decode64("QR==")
# => ArgumentError を送出(パッドビットがゼロではない)
デコーダーが雑であれば、2 つ目の文字列は 1 つ目と同じバイトにデコードされてしまいます。Ruby は雑ではありません。実際にはこれが、自分でエンコードしなかった入力に対する strict_decode64 の正しいデフォルトになる理由です:誤字、切り詰め、アルファベットの不一致を、静かな破損の代わりに、大きくてキャッチできるエラーに変えてくれるからです。
urlsafe_decode64:外交官
Base64.urlsafe_decode64 は、+ と / が予約されている場所を旅するペイロードのためにあります:URL、トークン、データベースの識別子。内部では URL 安全なアルファベット(ハイフンとアンダースコア)を標準アルファベットに戻して翻訳し、パディングを正規化して、結果を厳格なデコーダーに渡します:
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ")
# => "Hello world"
Base64.urlsafe_decode64("SGVsbG8gd29ybGQ==")
# => ArgumentError を送出(15 文字にはパッド 1 文字が必要で、2 つではない)
1 番目の例が見せるのが、いちばん便利な性質です:パディングのない入力でも問題ない。パディングがなく、長さが 4 の倍数でなければ、デコーダーが足りない = 文字を代わりに足してくれます - URL 安全な Base64 の最大の消費者である JSON Web Token が生成するのが、まさにそういう出力です。ただしパディングが存在する場合は正しくなければなりません。厳格なデコーダーと同じです。
ドキュメントが大きな声で言っていない癖が 1 つあります:外交官は二か国語を話します。このメソッドは厳格デコードの前にハイフンとアンダースコアを書き換えるので、標準アルファベットの文字列も受理します:
Base64.urlsafe_decode64("aGVsbG8=")
# => "hello" 標準アルファベットも受理される
その寛大さは便利ですが、このメソッドではペイロードがどのアルファベットから来たのか判別できない、という意味でもあります。それが大切なら、デコードの前に自分で文字を確認してください。
そして decode64 と違い、外交官は空白に慈悲を示しません。URL 安全なペイロードの中に改行が 1 つでもあれば ArgumentError を送出します。入力が折り返されたファイルから来るなら、先に改行を取り除いてください。
バイトはテキストではない:文字コードの段階
ここでつまずくのは、経験のある開発者までです。Ruby がそれを可視にするからです。デコードされた Base64 文字列は、元のデータが PNG だろうと JWT のペイロードだろうと UTF-8 のラブレターだろうと、必ず ASCII-8BIT という文字コードのタグ(別名 BINARY)を持ちます:
bin = Base64.decode64(Base64.strict_encode64("h\u{e9}llo"))
puts bin.encoding
# => ASCII-8BIT
puts bin.bytes
# => [104, 195, 169, 108, 108, 111]
ペイロードがバイナリ - 画像、zip ファイル、ハッシュ - なら、そのままの形で保持して File.binwrite で書き出します。変換もなし、詮索もなし。ペイロードがテキストなら、バイトはほぼ確実に UTF-8 で、それを 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
この 2 つの呼び出しは違う仕事をしています。force_encoding はラベルを付け替えるだけで、valid_encoding? がそれから初めて、それが本物の UTF-8 を形成しているかを検証します。その順番で実行してください。BINARY 文字列を先に検証しても、検証できるものが何もないからです。そして生涯覚えておきたい小さな比較の罠が 1 つ:Ruby は BINARY 文字列が UTF-8 文字列と等しいとみなすのは、両方が純粋な ASCII の場合だけです。デコードしたテキストを元の文字列と比較する前に、先にラベルを付け替えてください:
decoded = Base64.decode64("aMOpbGxv")
puts decoded == "h\u{e9}llo"
# => false バイトは同じ、タグが違う
decoded.force_encoding("UTF-8")
puts decoded == "h\u{e9}llo"
# => true
JWT:キーなしでトークンを読む
JSON Web Token は、ドットで留められた 3 つの Base64 文字列です:ヘッダー、ペイロード、署名。最初の 2 つは JSON ドキュメントの URL 安全な、パディングなしの Base64 なので、トークンは誰に読まれてもおかしくありません - あなたにも、しかもどんなライブラリもなしで:
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"}
本番の仕事では jwt gem を使いましょう。実際にあなたを守る部分 - 署名 - とクレームの検証を、すべて処理してくれるからです:
# 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
ここに 2 つのセキュリティ注記を載せておかねばなりません。どちらも実際に人をインシデントに遭わせたことがあるからです。第一に、ペイロードは暗号化されていません。デコードすることは読むことで、解読(クラック)ではありません。唯一の保護は署名なので、デコードしたペイロードを信頼できる入力として扱うことは決してしないでください。第二に、JWT.decode でアルゴリズムを、上記のように完全にピン留めしてください。省略すると、トークン自身のヘッダーが検証方法を決めることになり、あの有名な JWT アルゴリズム混同攻撃が狙っているのは、まさにその 1 つの柔軟性です。
Basic 認証:平然と見える場所のパスワード
Web で最も古い認証ヘッダーは、Base64 そのものです。HTTP Basic 認証は、認証情報を user:password の形でエンコードして、Basic という単語の後ろに送ります - そしてこのヘッダーはリクエストごとに全部乗っているので、あなたがデバッグするあらゆるログに顔を出します。デコードは、剥がして分けるだけの仕事です:
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!
split の 2 という上限は大切です。パスワードにコロンが入ることは仕様上許されていて、切っていいのは最初の 1 箇所だけだからです。Ruby 自身の標準ライブラリも、Net::HTTP でこのヘッダーを逆方向に組み立てており、コアの pack テンプレートを直接使っています:
require "net/http"
request = Net::HTTP::Get.new("https://example.org/api")
request.basic_auth("alice", "s3cr3t!")
puts request["Authorization"]
# => Basic YWxpY2U6czNjcjN0IQ==
そして、明白であるからこそ言っておくべきセキュリティ注記:Base64 は翻訳者であり、鍵(ロック)ではありません。Basic 認証が許されるのは HTTPS の上だけです。このエンコードが存在するのは、認証情報が印刷可能なテキストとして線の上を渡せるようにするためで、秘密にしておくためではありません。
Data URI:ファイルではない画像
Data URI は、URL の中にファイルをまるごと隠します:メディアタイプ、base64 という単語、カンマ、そしてエンコードされたバイト。ブラウザは img タグや CSS の中でそれらをレンダリングし、1 ファイル HTML アプリは 2 番目のリクエストが一切不要になるのでそれを好みます。Ruby で作るのに 1 行で済みます:
require "base64"
png = File.binread("logo.png")
data_uri = "data:image/png;base64,#{Base64.strict_encode64(png)}"
デコードは逆ですが、人がつまずく細部が 2 つあります。カンマが区切りなのでちょうど 1 回だけ分割し、メディアタイプの部分は、何もないことも含めて何でもありです:
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)
ここでは decode64 ではなく strict_decode64 を使ってください:data URI のペイロードは 1 行のきれいなラインなので、壊れていたら大きなエラーが欲しいのです。さらにサイズの税金も忘れないでください - インライン化する画像はどれも約 1 分の 1 大きくなります - だから data URI はファビコンや小さなロゴ、フォントにはぴったりで、ヒーロー写真には悪い考えです。
メール:60 文字の行と mail gem
Base64 はメールのために発明されました。その傷跡が今でも見えます。SMTP は 7 ビットのテキストの短い行のために設計されたので、MIME Base64 は出力を短い行に折り返し、準拠したデコーダーは改行を無視しなければなりません。Ruby の decode64 はまさにそのように振る舞うので、折り返された MIME 本文は格好の餌です:
body = "Zm9vCmJh\ncgptYW4=\n"
Base64.decode64(body)
# => "foo\nbar\nman"
手で書くことはほとんどないでしょう。mail gem が MIME の仕事を全部やってくれます:添付ファイルは自動で Base64 エンコードされ、行は 60 文字で折り返されて - MIME の 76 文字の制限に余裕を持って収まり - 正しいヘッダーも添付されます:
# 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
# 添付ファイル部分は Content-Transfer-Encoding: base64 を持つ
同じトリックは、メールヘッダーの内部にも潜んでいます。ASCII 以外の件名は RFC 2047 のエンコードドワードとして到着します:キャラセット、文字 B、そして疑問符のあいだに Base64。手で 1 つデコードするのは、ちょっとした文字列の外科手術です:
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:鎧に包まれた鍵と証明書
鍵と証明書は、生きている時間のほとんどを PEM の鎧の中に過ごします:BEGIN 行、Base64 の塊、END 行。この鎧は 1980 年代からのもので、Privacy-Enhanced Mail がまさにこの Base64 系譜の出発点ですが、今でも .crt や .key ファイルがまとっている形式です。
PEM ファイルを手でデコードするのは、鎧を剥がして、寛容なデコーダーに改行をかじり切らせるだけです:
require "base64"
pem = File.read("server.key")
body = pem.lines
.reject { |line| line.start_with?("-----") || line.strip.empty? }
.join
key_bytes = Base64.decode64(body)
実際の使用では、手作業のステップをスキップして、PEM 文字列まるごとを OpenSSL に渡すのが普通です。OpenSSL は鎧を自分で読みます:
require "openssl"
key = OpenSSL::PKey.read(File.read("server.key"))
puts key.class
# => OpenSSL::PKey::RSA、あるいはその鍵が何であれ
知る価値のある相互運用の詳細は 1 つだけです:PEM の行は伝統的に 64 文字の長さですが、デコーダーは改行をいずれにせよ無視するので、60 文字での折り返しでも 1 つの巨大な 1 行でも、同じようにデコードできます。
ファイルと .b64 慣習
Base64 の世界で最も一般的なファイル形式は、1 つのエンコードされたペイロードを含む、.b64(または時々 .base64)拡張子のプレーンテキストファイルです。読むには 3 ステップの往復になります:
require "base64"
encoded = File.read("payload.b64")
bytes = Base64.decode64(encoded)
File.binwrite("payload.bin", bytes)
書き出しのほうでは File.binwrite を使ってください - デコードした PNG や zip はバイナリで、行末を翻訳するプラットフォームではテキストモードでの書き込みがそれを壊します。あなたの .b64 ファイルが行を折り返すツールから来たなら、decode64 は改行を無料でお任せしてくれます。寛容にするのではなく検証したいなら、ファイルをバイナリモードで読み、厳格デコードの前に改行を取り除いてください:
encoded = File.binread("payload.b64")
clean = encoded.delete("\r\n")
bytes = Base64.strict_decode64(clean)
Windows ではバイナリ読み込みが重要になります。テキストモードでは CRLF の行末が LF に書き換えられてしまう - それはまさに、検証しようとしている文字列の中で起きてほしくない変異の一種です。
URL 安全な Base64:リンクの中を旅するペイロード
これは URL 安全バリアントの、デコーダー側の話です。ここで下す選択が、3 つのデコーダーのどれに手を出すかを左右するからです。URL 安全な Base64(RFC 4648、第 5 節)は、URL が苦手な 2 文字を入れ替えます - + が - に、/ が _ に - そして通常、パディングも落とします。Ruby では、クエリパラメータ、cookie の値、API の識別子、YouTube 風の動画 ID、そしてもちろん JWT で出会います。
3 つのデコーダーが同じ入力にどう振る舞うかを示します。違いこそが、まさにバグが生まれる場所だからです:
| 入力 | decode64 | strict_decode64 | urlsafe_decode64 |
|---|---|---|---|
aGVsbG8=(標準、パディング付き) |
"hello" |
"hello" |
"hello" |
aGVsbG8(パディングなし) |
"hello" |
ArgumentError |
"hello" |
SGVsbG8gd29ybGQ-(最後のグループにハイフン) |
"Hello world"(1 バイト短い!) |
ArgumentError |
12 バイト、正解 |
aGVsbG8=\n(末尾の改行) |
"hello" |
ArgumentError |
ArgumentError |
aGVs!bG8=(余計な感嘆符) |
"hello" |
ArgumentError |
ArgumentError |
3 行目が人を噛む行です。URL 安全なペイロードを標準のデコーダーでデコードすると、何も送出する代わりに、最後のバイトが静かに失われます。なぜなら decode64 はハイフンを単に無視するからです。ペイロードが URL から来うるなら、urlsafe_decode64 でデコードしてください。
実践的な注記が 1 つ:URL 安全なペイロードを、標準アルファベットしか理解しない文脈(ライブラリ、他社のシステム)に移す必要がある場合、定番の相互運用トリック - アルファベットを翻訳して、パディングは自分で足す - は 3 行で済みます:
def standardize_urlsafe(b64)
b64 = b64.tr("-_", "+/")
b64 += "=" * ((4 - b64.length % 4) % 4)
b64
end
Base64.strict_decode64(standardize_urlsafe("SGVsbG8gd29ybGQ"))
# => "Hello world"
これが必要なことはほとんどありません - urlsafe_decode64 はすでにあなたのためにパディングを足してくれます - でも、これは他の人のコードで認識すべきパターンであり、相手側が標準アルファベットを期待しているときに手を出すべきパターンでもあります。
設定、環境変数、データベース
バイナリデータがテキスト文書の中に座らなければならないときは、Base64 が設定の中に現れます。.env ファイル、YAML 設定、JSON の設定の塊、どれも生のバイトを安全に載せられないので、バイトはエンコードされて、アプリケーションのどこかが起動時にデコードしなければなりません:
require "base64"
b64 = ENV.fetch("APP_LOGO")
bytes = Base64.decode64(b64)
File.binwrite("logo.png", bytes)
YAML は特別に言及に値します。この形式にはネイティブのバイナリタグがあるからです。BINARY 文字列を dump すると、Psych は Base64 を持つ !binary スカラーとして書き出し、読み込めばあなたのバイトはそのまま返ってきます - 手動のエンコードは一切不要:
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
データベースでの基本ルールはこうです:データベースに本物のバイナリ型があるなら、それを使いましょう。TEXT 列に Base64 を入れるのは、ストレージ層が文字列しか話さないときに手を出すパターンです - ある種のドキュメントストア、JSON 型の API、あるいは変えられないレガシーなスキーマ - その代価は、列にかかる 3 分の 1 のサイズ税と、すべての境界で入るときはデコードし、出るときは再エンコードするという規律です。
大きな入力、安定したメモリ
このモジュールはバッファベースです:デコードの呼び出しは文字列を 1 回ですべて読んで、結果をまるごと返します。標準ライブラリにストリーミングのデコーダーはないので、大きなペイロードへの誠実な助言は、メモリを計画しなさいというものです。朗報は、デコードは常にものを小さくするということです - 出力は入力の 3 分の 4 が最大 - だから、大きな割り当ては入力文字列だけです。
ペイロードが心配になるほど大きいなら、4 文字グループでデコードできます。Base64 の 4 文字グループは自己完結していて、最後の部分的なグループは自分自身のパディングを持っているからです:
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
これはきれいで折り返されていない入力で機能します - strict_decode64 が強制するのと同じルールです - 単独の末尾グループは、パディングが存在するときのみ有効だからです。本当に巨大なファイル、マルチギガバイトのアーカイブのようなものでは、ファイルをスライス単位で読んで、各スライスをデコードし、バイトをディスクへストリーム書き出し、メモリには常に 1 つのスライスのみを持つ、というのがパターンです。
ターミナルのワンライナー
シェルでデコードするのにスクリプトファイルは不要です。Ruby はモジュールをその場で require できます:
ruby -rbase64 -e 'puts Base64.decode64(ARGV[0])' "aGVsbG8gd29ybGQ="
# => hello world
ファイルの場合は、ペイロードそのものではなくファイルパスを渡します:
ruby -rbase64 -e 'print Base64.decode64(File.read(ARGV[0]))' payload.b64 > payload.bin
ここに 2 つの落とし穴が住んでいます。第一に、echo やどんなテキストコマンドかをパイプに流すと、末尾の改行が同伴してきて、strict_decode64 はそれに対して送出します - decode64 を使うか、入力を chomp してください:
echo "aGVsbG8gd29ybGQ=" | ruby -rbase64 -e 'print Base64.strict_decode64(STDIN.read.chomp)'
第二に、バイナリ出力には puts ではなく print を使い続けましょう。puts は改行を 1 つ自分で足すので、復元したファイルの終わりを壊してしまいます。
Ruby 開発者が実際にひっかかる落とし穴
- decode64 は決して送出しない。ゴミが入ればゴミが出る。入力が信頼できず、破損したバイトを黙って受け入れるなら、そのバグはデコードの行でではなく、数週間後に壊れたファイルの中で顔を出します。自分でエンコードしていないものには、デフォルトで厳格なデコーダーを使いましょう。
- strict_decode64 と末尾の改行。テキストファイル、echo パイプ、コピーペーストはどれも改行で終わるのを好み、厳格なデコーダーはそれに対して
ArgumentErrorを送出します。先にchompしてください - それともバイナリモードで読んで改行を削除する。 - 文字コードの段階を忘れる。デコードされた文字列は、あなたがそう言わない限り BINARY のままです。結果をテキストとして扱う前に UTF-8 に強制して(有効性も確認して)ください。さもなくば、UTF-8 文字列と混ぜた瞬間に文字化けと
Encoding::CompatibilityErrorが待っています。 - BINARY と UTF-8 の比較。同じバイト、異なるタグで、
==は false を言います - 文字列がたまたま純粋な ASCII でない限り。比較する前に付け替えラベルを付けてください。 - URL 安全な入力を間違ったデコーダーで。ハイフンとアンダースコアは
decode64に黙って捨てられるので、URL 安全なペイロードはエラーもなしに 1 バイト短く、破損した形で戻ってきます。urlsafe_decode64を使ってください。 - パディングより後のデータは見えない。
decode64は最初の=で止まります。MIME には最高ですが、他のツールに切り詰められてから再パディングされたペイロードを捕まえるのには最悪です。 - 正典でないパディングが静かに受理される。
QR==のような文字列は、まっとうなエンコーダーがゼロにすべきパッドビットを持っています。decode64はそれを喜んでデコードし、strict_decode64はそれを拒否します。あなたのエンコーダーが嘘をついていたことを教えてくれるものはありません。 - Windows でのテキストモードのファイル読み込みは、あなたが見る前から行末を書き直してしまいます。
.b64ファイルを検証しようとするなら、バイナリモードで読んでください。
デコード側のいい習慣
- データの出所からデコーダーを選ぼう:信頼できないものはすべて
strict_decode64(そして無効入力ブランチとしてArgumentErrorを rescue する)、URL で生まれたペイロードはurlsafe_decode64、MIME 本文のような本当に寛容な形式だけにdecode64。 - バイトがデコードされた瞬間に、その正身を決めよう:バイナリ(ASCII-8BIT を保持し、
File.binwriteで書く)かテキスト(UTF-8 にforce_encodingしてから、使用前にvalid_encoding?)。 - デコードしたからといって信頼するな。JWT のペイロードが読めるのは、まさにそれが Base64 だからで、本物かどうかを決めるのは署名です。設定ファイルの中の Base64 文字列はデータであって、証拠ではありません。
- バリデータを書くときは、退屈なケースでテストしよう:空の文字列、パディングのない入力、折り返された入力、URL 安全な入力、誤ったパディング。それらがまさに、3 つのデコーダーを分けるケースです。
Ruby における Base64 の短い歴史
Base64 モジュールは 15 年以上、Ruby の標準ライブラリの一部であり続けてきました。そしてお届けのされ方は、あなたが思っているより何度も変わっています:
- 2008 年、Ruby 1.8.7: モジュールは
encode64、decode64と、もう存在しない 2 つのメソッド -b64encode(選んだ行長で折り返す)とdecode_b(RFC 2047 メールヘッダーのデコード)- と一緒に同梱されていました。古い本や、古い gem のいくつかで今でも参照されていますが、今どちらを呼び出してもNoMethodErrorです。 - 2009 年、1.9 シリーズ:
strict_encode64、strict_decode64、urlsafe_encode64、urlsafe_decode64が参入し、2 つのレガシーメソッドは引退しました(1.9.1 は 2009 年 1 月にすでに両方の変更をリリース済み)。 - 2015 年、Ruby 2.3:
urlsafe_encode64がpadding:キーワードを手に入れ、トークンや URL 向けにパディングなしの出力をできるようになりました。 - 2020 年、Ruby 3.0: base64 は標準ライブラリから自分の gem として抽出され、
ruby/base64リポジトリ配下のバージョン 0.1.0 になりました。default gem として同梱されるので、require "base64"はそのまま動きます。 - 2023 年、Ruby 3.3: バージョン 0.2.0 が
Base64::VERSIONと、ずっと豊かなドキュメント群を追加しました。 - 2024 年、Ruby 3.4: この gem は default gem から bundled gem に再分類されました。実践的な影響:Ruby 3.4 以降の Bundler ベースのプロジェクトでは、Gemfile に
gem "base64"を載せる必要があります(またはgem install base64でインストール)。 - 2025 年、Ruby 4.0: バージョン 0.3.0 が到着し、RBS 型シグネチャなど、メンテナンスも加わりました。
そのすべてを通して、1 つの事実は一切変わりませんでした:このモジュールは、コアの pack と unpack テンプレートのうえに乗る、数十行の純 Ruby ということです。C 拡張なし、依存関係なし、ビルドすべきものなし - そして rubygems.org でのダウンロード数は数億回に達しています。
好奇心旺盛な人向け Ruby トリビア
- このモジュールのデコード側は、1 行のメソッド本体 2 つ、
str.unpack1("m")とstr.unpack1("m0")、それに urlsafe バリアント - 厳格版の上の文字交換とパディング修正 - でできています。require を消して自分で書けます。 - Ruby 自身の
Net::HTTPは、Basic 認証のためにBase64モジュールすら使っていません -packテンプレートを直接呼び出します:["user:pass"].pack("m0")。 - Rails の署名付き・暗号化 cookie は、中身は Base64 文字列です:ActiveSupport のメッセージコデックは通常の cookie に
strict_encode64を選び、URL 安全な署名 ID にはpadding: falseのurlsafe_encode64を使います。あなたが気づかずに 1 つデコードしたことがあるかもしれません。 - すべてのダイジェストクラスに
base64digestメソッドがあります -Digest::SHA256.base64digest("hello")- テキストの中に住まなければならないチェックサム用のワンライナーです。 - YAML の
!binaryタグは Base64 です。BINARY 文字列を Psych で dump すると、形式がそっとエンコードしてくれます。 decode64は、あなたの行が 60 文字、64 文字、76 文字、あるいは 1 つの巨大な 1 行だろうと気にしません。mテンプレートが改行をスキップするので、折り返された入力と、折り返されていない入力とが同じようにデコードされます。
つづけていこう
これであなたは、デコードのツール一式をすべて持っています:MIME 形の塊のための寛容な読み手、信頼できないものすべてのための厳格な番人、トークンとリンクのための URL 安全な外交官、そして結果のバイトを Ruby が使えるテキストに変える文字コードの段階。逆方向 - Ruby の 3 つのエンコーダーのうち、どのエンコーダーに自分のバイトを与えるかを決め、アルファベット、パディング、改行を制御すること - には、誰も求めなかった末尾の改行から始まる、自分だけのサプライズのセットが待っています。通りの向こう側は、下にリンクする Base64 エンコードの記事で、同じ深さで扱っています。
最終更新: 2026-10-10