Base64の仕組み
Base64はバイナリデータをテキストに変換し、文字しか扱えない経路でも通せるようにします。仕組みはRFC 4648で定義されています:3バイト(24ビット)ごとに、64種類の記号からなるアルファベットのうち4文字へ変換し、バイト数が割り切れないときは=をパディングとして付けます。データは約3分の1増えます。これはエンコードであって、暗号化ではありません。
Base64の仕組みとは
1980年代のメールシステムは7ビットのテキストしか運べませんでした。HTTPヘッダー、JSONフィールド、URL、ログファイルも、実運用ではテキスト専用の領域です。画像、署名、暗号鍵といった生のバイナリは、そうした経路に触れた瞬間に壊れます。Base64はその標準的な橋渡し役です。64種類の安全な印刷可能文字だけで任意のバイトを綴る、完全に可逆な方法です。
Base64は、意識しないうちに一日中読んでいるはずです。あらゆるJWTは3つのBase64urlセグメントですし、CSSに埋め込まれたすべてのdata: URI、MIMEメールの添付もそうです。この方式はRFC 4648で規定されています(それ以前のBase-64の定義を1文書に統合したもの)で、手計算できるほど小さな仕組みです。このガイドはまさにそれを行います。24ビットのグループをひとつずつ。
最初に誰もがつまずくのは3点です。3バイトが4文字になる理由、=の記号が何のためのものか、そしてアルファベットが2種類ある理由。それぞれ以下に専用のセクションを設け、正直な限界——サイズの増加ペナルティ、デコーダ間での空白の扱いの食い違い、可逆エンコードをセキュリティとして扱ってしまう根深い誤解——とともに解説します。
ここでは説明するより読むほうが早い——任意の文字列をBase64デコーダーに貼り付けて、隠れているバイト列を観察してみてください。ローカルでデコードされるため、トークンや鍵はタブの中から出ません。
Base64の最も有名な利用者はトークン形式です。JWT構造ガイドは、JWTの3つのセグメントが後述のURLセーフなアルファベットの上にどう成り立っているかを示します。
仕組み:3バイトが4文字になる
任意のバイナリデータを3バイト(24ビット)ずつのグループに切り分けます。各グループを4つの6ビットの塊に分割すると、それぞれの塊は0から63までの数値になります。RFC 4648のアルファベットは、この数値を文字に対応させます。0–25はA–Z、26–51はa–z、52–61は0–9、そして62と63は+と/です。6ビットが最適なスイートスポットなのは、2⁶ = 64が、文字しか扱えない経路でも文句を言われずに運べるアルファベットのサイズにちょうど一致するからです。
定番の実例はRFC自身のテストベクタ、単語「Man」です。3バイトで、パディングは不要:
バイト単位
M 0x4D 01001101 a 0x61 01100001 n 0x6E 01101110 joined: 010011 010110 000101 101110 indexes: 19 22 5 46
文字単位
19→T 22→W 5→F 46→u "Man" → TWFu "Ma" → TWE= "M" → TQ==
パディング:=の記号が意味するもの
6ビットの塊は、4個ちょうどのグループでない限りバイトに再構成できません。入力の長さが3の倍数でなければ、最後のグループは足りなくなります。残り2バイトなら3文字に=が1個、残り1バイトなら2文字に==が付きます。パディングは長さを補うことで、デコーダが推測なしに正確なバイト数を計算できるようにします。そして=は、64種類の記号からなるアルファベットの外側で、出力に現れてよい唯一の文字です。
パディングは長さのメタデータであり、セキュリティでも圧縮でもありません。RFC 4648は、双方が合意すればパディングなしのバリアントを許しています。JWTのBase64urlはその代表例で、=を丸ごと省きます。ドットがセグメントを区切り、各セグメントの長さがデコーダに必要な情報をすべて伝えるからです。
| 入力バイト数 | 出力文字数 | パディング | 例 |
|---|---|---|---|
| 3(3の倍数ならどれでも) | 3バイトにつき4文字 | なし | "Man" → TWFu |
| 2 | 3 | =が1個 | "Ma" → TWE= |
| 1 | 2 | =が2個 | "M" → TQ== |
| 12 | 16 | なし | "Hello, world" → SGVsbG8sIHdvcmxk |
2種類のアルファベット:標準とURLセーフ
標準のアルファベットは+と/を使います。メールでは無害ですが、URLでは厄介者です。URLでは+は空白としてデコードされ、/はパスの区切りとして扱われます。そのためRFC 4648 §5はBase64urlを定め、+を-に、/を_に置き換えます。URLの中でパーセントエンコーディングが不要で、ファイル名やCookieの中で引用符も不要な文字です。それ以外の点で、2つの方言は同一です。同じ6ビットの塊、同じパディングのルール。
ここに実用上の罠があります。2つの方言が異なるのはわずか2文字で、間違ったアルファベットのままトークンを貼り付けると、2文字目で失敗します。もっと悪いことに、たまたま+や/を含んでいれば、違うバイトにデコードされてしまいます。行儀の良いデコーダーはどちらの形式も受け付け、どちらを見たかを教えてくれます。厳格なものは推測をせず、混在したアルファベットを丸ごと拒否します。
| 値 | 標準 | URLセーフ |
|---|---|---|
| 0–25 | A–Z | A–Z |
| 26–51 | a–z | a–z |
| 52–61 | 0–9 | 0–9 |
| 62 | + | - |
| 63 | / | _ |
同じバイト、2つの方言
bytes: 0xFB 0xEF 0xBE standard: ++++ url-safe: ----
方言の違いが出る箇所
URLのクエリでは、++++ は4つのスペースとして届く (プラスをスペースに変える規則)——---- はそのまま届く。 トークンがURLセーフなアルファベットを使うのはこのため。
サイズ:3分の1の税とその但し書き
4文字が3バイトを運ぶため、エンコード後の出力は常に入力の4/3——つまり約33%増——になります。これは転送オーバーヘッドが乗る前の話です。具体的な目安を挙げると、100バイトは136文字に、17バイトは24文字になります。この比率は決して動きません。チューニングではなく算術だからです。パディング、改行、折り返しはさらに上乗せします。MIMEはBase64の行を76文字で打ち切り、行間にCRLFを要求するため、大きなペイロードでは少し余分に増えます。data: URIは、すでに膨らんだその形式をCSSやHTMLの中に埋め込みます。インライン化した画像が、置き換えられる元のバイナリファイルより約3分の1重くなるのはこのためです。
可逆な処理でデータが縮むことはなく、Base64も例外ではありません。これは綴りの変更であって、圧縮ではありません。サイズが問題になるなら、先に圧縮して、圧縮後のバイトをエンコードします。逆の順序は決して踏まないことです。
空白、改行、そしてデコーダ間の不一致
RFC 4648は厳格です。デコーダーはアルファベット外の文字を拒否しなければならず、文書自体は改行を要求するとはどこにも書いていません。MIMEのBase64(RFC 2045)は正反対の前提で、76文字ごとのCRLFを期待します。実際の実装はその両極の間に散らばります。黙って空白を剥ぎ取るもの、空白を要求するもの、改行を一切受け付けないもの。あるツールでは「問題なくエンコードできる」文字列が、別のツールではデコードに失敗する。定番の理由がこれです。
誠実な相互運用のルールはこうです。仕様が別の指示をしない限り、標準的で、パディング付きで、折り返しのないBase64を出力すること。そして、他のツールが付けた空白を許容するデコーダーを作ること。ただし、認められたアルファベットに属さない文字は依然として拒否します。ここで寛容になると、気づかないバイト破損が始まるからです。
Base64ではないもの
Base64には鍵がなく、攪拌のラウンドもなく、秘密もありません。誰でも逆変換できますし、デコーダーはあらゆる言語の標準ライブラリに同梱されています。エンコード済みのペイロードを保護されたデータとして扱うことは、繰り返し報告される脆弱性です。見分けるサインは公然のものです。SGVsbG8=はHelloを綴り、JVBERi0xはPDFの始まり、/9j/はJPEGの始まり、そしてeyJはほぼすべてのJSON Web Tokenの始まりです。これらの先頭文字は、%PDF-とJPEGのマジックバイトとJSONの開き波括弧をBase64で綴ったものにすぎません。
Base64は本来の役割で使ってください。バイト列を、どんな容器でも安全に通る綴りにするのがその役目です。機密性は本物の暗号化に任せます。転送中はTLS、保存時はまともな暗号方式を。Base64は、その結果を印刷可能にする最後の工程としておきます。それ以上ではありません。
正体がわかる先頭の文字
SGVsbG8sIHdvcmxk JVBERi0xLjQ= /9j/ eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
デコードすると
Hello, world
%PDF-1.4
(バイト列 FF D8 FF:JPEGのマジックナンバー)
{"alg":"HS256","typ":"JWT"}よくある質問
トークンや鍵をBase64デコーダーに貼り付けても安全ですか?
このサイトでは安全です。デコーダーはブラウザ内で動作し、入力を送信することは一切ありません。漏えいしうるアップロード先が存在しないからです。デコード中もNetworkパネルにはリクエストが表示されず、サイトのContent-Security-Policyが外部への接続をブロックします。これが重要なのは、Base64のトークンは生きている認証情報であり、ネット上の無数のデコーダーの多くは、その扱いにはるかに甘いからです。
Base64は暗号化ですか?
いいえ。鍵を持たず、秘密性もないエンコードです。どんなライブラリでも、注意深い人間でも、即座に逆変換できます。エンコードされたパスワードは、丸見えのパスワードです。暗号化は鍵なしでは読めないようにデータを変換しますが、Base64はバイトの綴りを変え替えて、テキスト専用の経路でも運べるようにするだけです。両者は別々の問題を解決するものであり、しばしば並んで登場します。
Base64の出力が=の記号で終わるのはなぜですか?
=は長さを補うパディングです。出力は4文字単位のグループで構成され、入力が3バイトの倍数でなければ最後のグループが埋め足されます。残り2バイトなら=が1個、残り1バイトなら2個です。3個になることはありません。完全なグループには不要だからです。この記号はデータを運びません。デコーダーに正確なバイト数を伝えるためのものです。
Base64とBase64urlの違いは何ですか?
2文字の違いです。RFC 4648 §5で定義されるBase64urlは、+を-に、/を_に置き換えます。これにより出力は、エスケープなしでURL、ファイル名、Cookieに安全に載ります。それ以外はすべて同一です。6ビットのグループ化も、英字と数字も、パディングのルールも。そしてJWTなどの形式は、パディングを取り除いたURLセーフ版を使います。
Base64の出力が入力より大きくなるのはなぜですか?
出力の1文字が運べるのは6ビットで、バイトは8ビットだからです。そのため3バイト——24ビット——には4文字が要ります。パディングと改行を加える前の、固定の4/3、すなわち約33%のオーバーヘッドです。印刷可能であり続けるための代償であり、これを避けた可逆的なエンコードは存在しません。だからこそ、経路が実際のバイトを許すところでは、生のバイナリ転送が勝つのです。
Base64を手作業でデコードできますか?
はい。設計そのものが手計算を歓迎しています。入力のバイトを2進数で書き出し、24ビットのブロックごとに4つの6ビットの塊へ組み替え、アルファベットの表から各塊を読み取ります。A=0からZ=25、a=26からz=51、0=52から9=61、そして+と/です。RFC自身の例なら紙の上でも再現できます。Manは010011 010110 000101 101110であり、これはT、W、F、uと読めます。つまりTWFuです。