Base64エンコードとは何か
Base64エンコードを一言で説明すると、任意のバイナリデータを64種類の印字可能なASCII文字列に変換する仕組みです。暗号化でも圧縮でもなく、バイトからテキストへの可逆的な変換に過ぎません。名前の由来は使用する64文字のアルファベット(A-Z、a-z、0-9、そして+と/の2文字)にあります。
このアルゴリズムはRFC 4648(2006年公開、それ以前のRFC 3548を置き換え)で正式に定義されています。Base64が存在する理由は、メールプロトコル、JSONペイロード、XMLドキュメント、URLパラメータなど多くのシステムがテキスト専用に設計されているためです。PNG画像をメールに添付したり、PDFをJSON APIレスポンスに埋め込む際に、バイナリデータをプレーンテキストとして表現する手段を提供します。代償はサイズの増加で、入力3バイトが出力4文字になります。
ここで多くの入門記事が省略するポイントがあります。Base64はUTF-8やASCIIと同じ意味の「エンコード」ではありません。UTF-8やASCIIは文字をバイトにマッピングしますが、Base64はバイトを文字にマッピングします。方向が逆なのです。UTF-8はコンピュータがテキストを保存するためのもので、Base64はテキストベースのシステムがバイナリを運搬するためのものです。
Base64エンコードの仕組み(ステップバイステップ)
アルゴリズムは入力を3バイト(24ビット)ずつ処理します。24ビットを4つの6ビットグループに分割し、各6ビット値(0〜63)をBase64アルファベットの1文字にマッピングします。入力3バイトが出力4文字になるため、サイズは必ず33%増加します(パディングを含むとさらに増える場合も)。
入力の長さが3で割り切れない場合、パディングが発生します。1バイト余る場合は2文字のBase64出力と「==」が生成されます。2バイト余る場合は3文字のBase64出力と「=」が生成されます。パディング文字はデコーダに末尾で何バイト破棄すべきかを伝えます。base64urlなど一部の実装ではパディングを省略します(長さ自体が暗黙的に示すため)。
例として「Hi」(2バイト:0x48 0x69)のエンコードを追跡してみましょう。2進数で01001000 01101001です。24ビットにするためゼロで埋めて01001000 01101001 00000000。6ビットずつ分割:010010 000110 100100 000000。インデックスに変換:18, 6, 36, 0。アルファベットで参照:S, G, k, A。パディング1バイト分の「=」を追加。結果は「SGk=」です。
最初に自分で実装した時、パディングのゼロが最後の6ビットグループの一部であり別個のものではないことを忘れてハマりました。デコーダは最後の「A」がパディングゼロを表し、実際のゼロバイトではないことを知る必要があります。それを知らせるのが「=」の役割です。
// "Hi"のエンコード手順
const input = "Hi";
const bytes = [0x48, 0x69]; // H=72, i=105
// ステップ1: 2進数に変換(24ビットにパディング)
// 01001000 01101001 00000000
//
// ステップ2: 6ビットグループに分割
// 010010 | 000110 | 100100 | 000000
// 18 6 36 0
//
// ステップ3: Base64アルファベットにマッピング
// S G k A
//
// ステップ4: パディング追加(1バイト分のパディングあり)
// 結果: "SGk="
console.log(btoa("Hi")); // "SGk="
console.log(atob("SGk=")); // "Hi"
// Node.jsの場合:
Buffer.from("Hi").toString("base64"); // "SGk="
Buffer.from("SGk=", "base64").toString(); // "Hi"33%のサイズ増加とその影響
入力3バイトごとに出力4バイトが生成されます。パディングや改行を含む前の段階で、固定33.3%の増加です。1 MBの画像をBase64にすると最低1.33 MBになります。10 MBの動画サムネイルをJSONレスポンスに埋め込むと、13.3 MBがネットワークを流れることになります。これは積み重なると無視できません。
以前、モバイルアプリのデータ消費量が異常に多い問題をデバッグしたことがあります。原因はAPIがユーザーアバターをJSON内のBase64文字列として返していたことでした。200 KBのアバターが267 KBのBase64テキストになり、20件のアバターを含むJSONレスポンスは5.3 MBに膨れ上がっていました。CDN経由の画像URLに切り替えたところ、ペイロードは4 KBまで削減できました。教訓:JSONにバイナリを埋め込めるからといって、そうすべきとは限りません。
見落とされがちな第二のコストがあります。Base64文字列はストリーミングや部分デコードができません。バイナリファイルなら、残りのダウンロード中に最初のバイトからレンダリングを開始できます。Base64では通常、デコード前に文字列全体が必要です。低速な接続で大きなペイロードを扱う場面では、この差が顕著になります。
Base64を使うべき場面(実際のユースケース)
メール添付ファイル(MIME):これが元々のユースケースです。SMTPは7ビットASCIIテキスト用に設計されました。バイナリの添付ファイルはBase64エンコードされ、8ビット目を除去する可能性のあるメールサーバーを無事に通過できます。これまでに送信したすべてのメール添付ファイルは、内部でBase64を使用しています。
HTML/CSSのData URI:小さな画像をマークアップに直接埋め込むことで、追加のHTTPリクエストを回避できます。2 KBのアイコンをData URI(data:image/png;base64,...)にすると1往復分のリクエストが節約されます。ただし5 KB以上のファイルは通常、別ファイルとして配信した方が良いでしょう。Base64のオーバーヘッドに加え、個別にキャッシュできないため、トータルではマイナスになります。
JSONおよびXMLペイロード:APIがテキストベースフォーマットにバイナリデータ(署名画像、小さなファイル、暗号鍵など)を含める必要がある場合、Base64が標準的なアプローチです。AWS S3のPresigned POSTポリシーはBase64エンコードされたJSONを使用します。JWTもヘッダーとペイロードをBase64urlでエンコードしています。
HTTP Basic認証:AuthorizationヘッダーはBase64エンコードされた「username:password」を送信します。これは暗号化ではありません。ヘッダーを傍受した人は即座にデコードできます。パスワード内の特殊文字がHTTPヘッダー形式を壊さないようにするためのエンコーディングに過ぎません。Basic認証は必ずHTTPSと組み合わせて使用してください。
Base64を使うべきでない場面(よくある間違い)
セキュリティ目的にBase64を使わないでください。APIキーをBase64エンコードして「暗号化」している本番コードを見たことがあります。これはセキュリティゼロです。atob()でBase64文字列をデコードするのにマイクロ秒しかかかりません。データを保護するなら本物の暗号化(AES-256-GCM)やハッシュ化(SHA-256)を使ってください。Base64はエンコーディングであり、暗号化ではありません。
JSONレスポンスに大きなファイルを埋め込まないでください。APIが画像、PDF、動画を返す場合は、適切なContent-Typeヘッダー付きの個別バイナリレスポンスとして提供しましょう。CDNやオブジェクトストレージへのURLを使用してください。33%のオーバーヘッド、独立してキャッシュできない点、大きな文字列によるメモリ圧迫、これらすべてが数KB以上のインラインBase64に反対する論拠です。
URLではbase64url変換に切り替えずに標準Base64を使わないでください。標準Base64は+と/を使用しますが、これらはURLで特別な意味を持ちます。base64url(RFC 4648 §5)はこれらを-と_に置き換え、=パディングを省略します。JWTがbase64urlを使うのはこの理由です。標準Base64をURLエンコードせずにクエリパラメータに入れると、データが破損します。
既にテキストのデータをBase64エンコードしないでください。JSONをBase64エンコードしてからJSONフィールドに格納するコードをレビューしたことがあります。二重エンコードされたデータは33%大きくなり、デバッグも困難です。データが既に有効なテキスト(UTF-8文字列、JSON、XML)であれば、そのまま含めてください。
Base64の変種:標準、URL安全、MIME
RFC 4648は複数のBase64アルファベットを定義しています。標準アルファベット(Table 1)はA-Z、a-z、0-9、+、/を使用し、=をパディングとします。これがbtoa()やほとんどのライブラリがデフォルトで生成するものです。
URL安全アルファベット(Table 2、通称「base64url」)は+を-に、/を_に置き換え、URL構文との衝突を回避します。パディングの省略はオプションです。JWT、OAuthトークン、Base64データがURLやファイル名に含まれるすべての場面で使われます。Node.jsではBuffer.from(data).toString("base64url")で利用できます。
MIME Base64(メールで使用)は標準アルファベットですが、76文字ごとに改行が入ります。古いデコーダは改行なしのBase64で失敗し、モダンなデコーダは改行ありのBase64で失敗する場合があります。消費側がどの変種を期待しているか、常に確認してください。
Base32(RFC 4648 §6)という選択肢もあります。大文字アルファベットと数字2-7のみを使用します。サイズは入力の60%増(Base64の33%に対して)ですが、大文字小文字を区別せず、0/Oや1/lのような紛らわしい文字を避けられます。TOTPコード(Google Authenticator)は共有シークレットにBase32を使用しますが、これは人間が手入力する必要があるためです。
各言語でのBase64実装
JavaScript(ブラウザ):btoa()で文字列をBase64にエンコード、atob()でデコードします。注意点:btoa()はLatin-1文字しか処理できません。UTF-8文字列の場合はTextEncoderを使ったアプローチが必要です。2024年以降、多くの環境でTextEncoder/TextDecoderのbase64エンコーディングオプションがサポートされています。
Node.js:Buffer.from(data).toString("base64")でエンコード、Buffer.from(b64, "base64")でデコードします。base64urlの場合は"base64url"を指定します。btoa()のLatin-1制限なしにバイナリデータを正しく処理できます。
Python:import base64; base64.b64encode(bytes_data)とbase64.b64decode(b64_string)を使用します。URL安全版はbase64.urlsafe_b64encode()です。bytesオブジェクトで動作するため、.encode("utf-8")と.decode("utf-8")の変換が必要です。
Go:encoding/base64パッケージでbase64.StdEncoding.EncodeToString()を使用し、URL安全版にはbase64.URLEncodingを使います。Goの実装は特に高速で、コンパイル言語の特性により大きな入力ではPythonの約3倍のスピードが出ます。
// ブラウザ: UTF-8を正しく処理する方法
const text = "Hello 你好 🌍";
// ❌ btoa()はLatin-1以外の文字で失敗する
// btoa(text) → InvalidCharacterErrorがスローされる
// ✅ UTF-8の正しいアプローチ
const encoded = btoa(
String.fromCharCode(...new TextEncoder().encode(text))
);
// "SGVsbG8g5L2g5aW9IPCfjI0="
const decoded = new TextDecoder().decode(
Uint8Array.from(atob(encoded), c => c.charCodeAt(0))
);
// "Hello 你好 🌍"
// Node.js: はるかにシンプル
Buffer.from(text).toString("base64");
Buffer.from(encoded, "base64").toString("utf-8");Base64のデバッグとトラブルシューティング
デコード後に文字化けする場合:文字エンコーディングの不一致が原因の可能性が高いです。最も一般的なケースは、UTF-8バイトとしてエンコードされたデータを、Base64出力後にLatin-1としてデコードする(またはその逆)パターンです。Base64エンコード前にUTF-8にエンコードし、Base64デコード後にUTF-8からデコードすることを徹底してください。
「Invalid character」エラー:空白文字を確認してください。MIMEフォーマットのBase64は76文字ごとに改行(\r\n)が含まれ、厳格なデコーダはこれを拒否します。デコード前にすべての空白を除去してください。また、変種の不一致も確認しましょう。-や_を含むbase64url文字列は標準Base64デコーダで失敗します。
パディングエラー:一部のデコーダはパディング(=)を要求し、別のデコーダはそれを拒否します。「incorrect padding」エラーが出る場合は、文字列の長さが4で割り切れるまで=を追加してみてください。=で「invalid character」が出る場合は、すべての=を除去してみてください。最も安全な方法は、デコード前にパディング付きの標準Base64に正規化することです。
二重エンコード:デコードした結果がBase64のように見える(英数字に+/=のみ)場合、誰かが二重にエンコードしています。もう一度デコードしてください。本番環境で三重エンコードされたデータを見たことがあります。各ミドルウェア層が「念のため」ペイロードをBase64エンコードしていたのです。当サイトのBase64ツールを使って、読み取り可能な出力が得られるまで反復的にデコードしてください。