Base64とBase64URLの違いと使い分け
Base64URLは標準のBase64の2文字を置き換えたもので、+を-に、/を_に変え、通常は=によるパディングを付けません。
どちらのエンコーディングもRFC 4648で定義されています。標準のBase64はセクション4、「URLおよびファイル名に安全な」変種であるbase64urlはセクション5です。両者はデータをまったく同じ方法で6ビットずつに区切り、64文字のうち62文字を共有しています。違うのは値62と63に割り当てられた文字と、パディングの慣習だけです。そのため相互の変換は文字を入れ替えるだけで済み、データを再エンコードする必要はありません。その反面、取り違えてもなかなか気づかれないバグになりやすいのです。
2つのアルファベットの比較
| Base64(RFC 4648、セクション4) | Base64URL(RFC 4648、セクション5) | |
|---|---|---|
| 値0~61 | A-Z a-z 0-9 | A-Z a-z 0-9 |
| 値62 | + | - |
| 値63 | / | _ |
=によるパディング | 必須(それを使う形式が別に定めていない限り) | 通常は省略 |
| そのままURLやファイル名に使えるか | いいえ | はい |
同じバイト列を両方の形式で表すと、次のようになります。
Bytes fb ff bf
Base64 +/+/
Base64URL -_-_
Text <<???>>
Base64 PDw/Pz8+Pg==
Base64URL PDw_Pz8-Pg
ハッシュ値、鍵、ランダムなトークンといったバイナリデータをエンコードすると、どこかに+や/が含まれることがごく普通にあるため、この違いは決して例外的なケースではありません。RFC 4648自身も、base64urlをbase64と同じエンコーディングとみなすべきではなく、コードは期待する変種を明示すべきだと述べています。
標準のBase64がURLやファイル名で壊れる理由
標準のBase64で使われる3つの追加文字は、それぞれ別の場所で特別な意味を持っています。
+は、クエリ文字列をフォームデータとしてデコードするとスペースとして読まれます。URLSearchParams、PHPの$_GET、そしてほとんどのサーバーフレームワークがこの読み方をします。ab+cのようなトークンはab cとして届きます。/はパスのセグメントを区切るため、パスに入れたBase64の値は余分なセグメントを作り、Webサーバーによってはパス中のエンコード形%2Fを拒否します。また、Windows、macOS、Linuxのいずれでもファイル名に使えません。=はパラメーター名と値を区切ります。
値をパーセントエンコード(%2B、%2F、%3D)する方法も、関係するすべてのコードがちょうど1回ずつ処理すれば機能します。しかし実際には、どこかの層が処理を忘れたり二重に処理したりします。だからこそ、URLでの利用を前提に設計された形式はBase64URLを使い、エスケープそのものを避けているのです。
パディングのルール
Base64は3バイトを4文字にエンコードします。データ長が3の倍数でないと最後のグループが短くなり、標準のBase64はそれを=で4文字まで埋めます。
| 最後のグループのバイト数 | Base64 | パディングなしのBase64URL |
|---|---|---|
1(例:ff) | /w== | _w |
2(例:fb ff) | +/8= | -_8 |
3(例:fb ff bf) | +/+/ | -_-_ |
RFC 4648のセクション3.2は、参照する仕様が明示的に別の定めをしない限り、パディングを必須としています。セクション5はさらに、URLでは=を通常パーセントエンコードしなければならないこと、そしてデータの長さがいずれにせよわかっている場合はパディングを省略できることを付け加えています。Base64URLの上に作られた形式はこの選択肢を採用しています。JSON Web Signature(RFC 7515)、したがってJWTは、末尾の=をすべて取り除きます。PKCEのコードチャレンジ(RFC 7636)も同様です。
パディングなしでは、nバイトが⌈4n/3⌉文字になります。16バイトは24文字ではなく22文字に、32バイトは44文字ではなく43文字になります。4で割った余りが1になる長さは、どちらの変種でも決して有効になりません。
それぞれが使われる場所
| 用途 | 変種 |
|---|---|
| JWTのヘッダー、ペイロード、署名 | パディングなしのBase64URL |
nやeなどのJSON Web Keyの値 | パディングなしのBase64URL |
OAuth PKCEのcode_challenge | パディングなしのBase64URL |
データURI(data:image/png;base64,...) | 標準のBase64(RFC 2397) |
| メールの添付ファイル(MIME) | 標準のBase64、1行最大76文字(RFC 2045) |
| PEM形式の証明書と鍵 | 標準のBase64、1行64文字(RFC 7468) |
| HTTPのBasic認証ヘッダー | 標準のBase64(RFC 7617) |
| URL、ファイル名、キャッシュキーに入れるトークン | Base64URLが安全 |
ライブラリーの既定は標準のBase64です。btoa、Pythonのb64encode、PHPのbase64_encode、JavaのBase64.getEncoder()はいずれも+、/、パディングを出力します。Base64URLが必要なら、明示的に指定しなければなりません。
相互の変換方法
atobとbtoaは標準のBase64しか扱えないため、これらを使う場合は文字の置き換えとパディングの調整を自分で行います。
// Base64 -> Base64URL
const url = b64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// Base64URL -> Base64
let std = url.replace(/-/g, '+').replace(/_/g, '/');
std += '='.repeat((4 - std.length % 4) % 4);
最近のブラウザーには、バイト列を直接扱えるUint8Array.prototype.toBase64({ alphabet: 'base64url', omitPadding: true })とUint8Array.fromBase64(s, { alphabet: 'base64url' })もあります。使う前に、対象とするブラウザーでの対応状況を確認してください。他のプラットフォームには、直接のサポートか短い定番の書き方があります。
// Node.js
Buffer.from(data).toString('base64url'); // no padding
Buffer.from(s, 'base64url');
# Python
base64.urlsafe_b64encode(data).rstrip(b'=')
base64.urlsafe_b64decode(s + '=' * (-len(s) % 4))
// PHP
rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
base64_decode(strtr($s, '-_', '+/'), true);
// Java
Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
Base64.getUrlDecoder().decode(s);
Pythonのurlsafe_b64decodeはパディングのない入力を拒否するため、=を補っています。PHPのbase64_decodeは、strictモードでもパディングの欠落を受け入れます。
落とし穴
- テストをすり抜けるバグ。標準のデコーダーがBase64URLの入力で失敗するのは、
-か_が含まれている場合だけです。ランダムなデータなら、16バイトの値のおよそ半分、SHA-256のダイジェストのような32バイトの値のおよそ4分の3が該当します(1 − (62/64)の42乗 ≈ 74%)。サンプル値が1つか2つのテストなら、簡単に通ってしまいます。デコーダーがこうした文字列を拒否する理由と、言語ごとの反応の違いは、Invalid Base64 stringで解説しています。 - 寛容なデコーダーが取り違えを隠す。Node.jsの
Buffer.from(s, 'base64')は両方のアルファベットを受け付けます。便利ですが、バグが別のサービスで初めて表面化することになります。Pythonの既定のb64decodeは-と_を黙って捨て、誤ったバイト列か、文字数に関する紛らわしいエラーを返します。 - Base64URLはURLエンコードではない。標準のBase64文字列をパーセントエンコードすると
%2B、%2F、%3Dになるだけで、Base64URLにはなりません。一方を期待するデコーダーは、もう一方を受け付けません。 - どちらの変種も何も隠さない。JWTのペイロードはBase64URLでエンコードされたJSONにすぎず、トークンを持っていれば誰でも読めます。JWTデコーダーに貼り付けると、ヘッダーとペイロードを確認できます。
このサイトのBase64エンコード・デコードツールは、パディング付きの標準のBase64を出力します。Base64URLが必要な場合は、上の置き換えを適用してください。デコードにはBase64デコードを使えば、両方のアルファベットを受け付け、欠けたパディングも自動で補うので、どちらの形式でも貼り付けられます。