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~61A-Z a-z 0-9A-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デコードを使えば、両方のアルファベットを受け付け、欠けたパディングも自動で補うので、どちらの形式でも貼り付けられます。

よくある質問

Base64とBase64URLの違いは何ですか?

Base64URLは+を-に、/を_に置き換えるため、結果をURLやファイル名に安全に使えます。また、通常は=のパディングを省きます。残りの62文字と、データを6ビットずつに区切る方法はまったく同じです。

通常のBase64デコーダーでBase64URLを読めますか?

変換してからなら読めます。-を+に、_を/に置き換え、長さが4の倍数になるまで=を追加してください。Node.jsのBufferのように、両方のアルファベットを直接受け付けるデコーダーもあります。

JWTがパディングなしのBase64URLを使うのはなぜですか?

トークンはURL、HTTPヘッダー、Cookieでやり取りされ、そこでは+、/、=が問題を起こすからです。RFC 7515は、JSON Web Signatureのエンコーディングを、末尾の=をすべて取り除いたbase64urlと定義しています。

Base64URLはBase64より長くなりますか?

いいえ。1バイトあたりの文字数はどちらも同じで、パディングがない分、Base64URLのほうが最大2文字短くなります。