Base64エンコードはなぜ4/3に膨らむのか、URL safe版との違い

約7分

Base64 は触る機会が多いわりに、「なぜサイズが膨らむのか」「+ /- _ に置き換える派生は何のためか」を都度確認したくなる技術です。仕様を理解しておくとデバッグや実装で迷わなくなるので、本記事では Base64 の中身を整理します。

そもそも Base64 が必要な理由

ネットワークやテキストプロトコル(メール、HTTP ヘッダ、JSON、URL)は、もともとバイナリデータをそのまま運ぶことを想定していないものが多くあります。例えば:

  • メール(SMTP)は7-bit ASCII を前提にしていて、8-bit のバイナリはそのままでは送れない
  • JSON 文字列に NULL バイトや改行を含めると、パースで壊れる
  • URL に ?& を含むデータをそのまま入れると、クエリパーサが誤動作する

これを回避するために、任意のバイナリを ASCII の安全な文字だけで表現する符号化が必要で、その代表格が Base64 です。仕様は RFC 4648 にまとまっています。

エンコードの基本:3バイト → 4文字

Base64 のアイデアは単純で、入力の 3 バイト(24 bit)を 6 bit ずつ4つに区切り、各 6 bit を1文字で表現するというものです。

入力:    [byte0       ][byte1       ][byte2       ]
         8 + 8 + 8 = 24 bit

出力:    [c0   ][c1   ][c2   ][c3   ]
         6 + 6 + 6 + 6 = 24 bit

6 bit が表現できる値は 2^6 = 64 通りで、これに A-Z a-z 0-9 + /64 文字を割り当てています(だから “Base64”)。

例:Man という3バイトの ASCII 文字列をエンコードすると:

'M'  = 0x4D = 01001101
'a'  = 0x61 = 01100001
'n'  = 0x6E = 01101110

ビット連結: 010011010110000101101110
6bit分割:   010011 010110 000101 101110
            = 19, 22, 5, 46
インデックス: T, W, F, u

→ "TWFu"

24 bit を 4 つの 6 bit に再分割して、テーブル引きするだけです。デコードは逆を行うだけなので対称的です。

なぜ 4/3 に膨らむのか

ここで気付くのは、入力の 3 バイト(24 bit)を出力の 4 文字(4 × 8 bit = 32 bit)で表現している点です。出力1文字は ASCII で 1 バイト(8 bit)必要で、6 bit のデータを 8 bit のスロットに入れるため、2 bit 分が無駄になります。

結果として、サイズの比率は:

出力サイズ / 入力サイズ = 32 / 24 = 4/3 ≈ 1.333

つまり Base64 は約33%サイズが増える符号化です。これは仕様上避けられないオーバーヘッドで、画像を data URI 形式で HTML に埋め込むと元の画像より大きくなるのはこのためです。

余りバイトの処理:パディング =

入力のバイト数が3の倍数とは限りません。残り1バイト・2バイトのケースをどう扱うかが、Base64 の地味にややこしい部分です。

入力余り処理出力
1 バイト(8 bit)6 bit + 2 bit を 8 bit にゼロ埋め → 2文字 + ==4 文字(うち 2 文字は =
2 バイト(16 bit)6 bit + 6 bit + 4 bit を 6 bit にゼロ埋め → 3文字 + =4 文字(うち 1 文字は =
3 バイト(24 bit)パディング不要4 文字

= は「パディング文字(このスロットは無効)」を意味するもので、デコード時に末尾の = の数を見て元のバイト数を復元できます。

実装では = を省略する Base64 もあり(“unpadded Base64”)、デコーダは入力長から余りを推測するか、エラーにするかを決める必要があります。後述する Base64URL ではパディングを省略するのが一般的です。

URL safe 版(Base64URL)の存在理由

標準の Base64 アルファベットには +/ が含まれますが、これは URL にそのまま入れるとマズいです。

  • + は URL のクエリ文字列でスペースを意味する(application/x-www-form-urlencoded
  • / はパスの区切り文字
  • =key=value の区切り文字

これらをそのまま URL に入れると、解釈が壊れたり別の意味になったりするので、URL に乗せる前にパーセントエンコードが必要になります。それを避けたいケースのために、Base64URL という派生規格が用意されています(RFC 4648 §5)。

標準 Base64Base64URL
+-
/_
= パディングあり= パディング省略が普通

JWT のヘッダ・ペイロードは Base64URL でエンコードされていて、-_ を見たら「これは URL safe 版だな」と判断できます。

デコード実装のときの注意

Base64URL を標準の atob()(ブラウザ)や Buffer.from(str, 'base64')(Node.js)でデコードする場合、事前に文字を戻す必要があります。

function fromBase64Url(s) {
	// -, _ を +, / に戻す
	let b = s.replace(/-/g, '+').replace(/_/g, '/');
	// 4の倍数になるよう = を付け足す
	while (b.length % 4) b += '=';
	return atob(b);
}

逆に標準 Base64 を Base64URL に変換するなら、+-/_、末尾の = を削除、の3ステップです。

エンコード != 暗号化

Base64 にまつわる一番大きな誤解は「Base64 でエンコードすればちょっと安全」というものです。Base64 は変換であって暗号ではありません。テーブル引きでデコードできるので、誰でも元のバイト列を復元できます。

ログに API キーを Base64 でエンコードして出すのは、平文で出すのと同じ危険度です。データを隠したいなら暗号化、識別子として使いたいだけならハッシュ、という使い分けを意識する必要があります。

ちょっとした文字列を試しにエンコード・デコードしたいときは、本サイトの Base64 ツールが手早く確認できます。すべてブラウザ内で処理されるので、機密データを試す場面でも外部に送られない点で安心して使えます。