Base64の仕組みとUTF-8対応 — btoa/atobの落とし穴と回避策
Base64の正体は「3バイトを4文字に変換」する符号化方式。RFC 4648で定義された仕組みと、JavaScriptで日本語を扱うときによく出るInvalidCharacterErrorの原因・回避策を実装コードつきで解説します。
1. Base64の3バイト→4文字変換の仕組み
RFC 4648 Section 4 はこう定義:「3つの8ビット入力グループを連結した24ビットを、6ビット×4の連結グループとして扱い、各6ビットをBase64アルファベットの1文字へマッピングする」。
つまり24ビットを4分割して0〜63の値を出し、その値を64文字のアルファベットから引きます。3バイトが4文字になるので、データサイズは約4/3倍(33%増)になります。
2. Base64アルファベット(RFC 4648 Table 1)
公式の対応表(Value→Encoding):
- ・0-25:
A〜Z - ・26-51:
a〜z - ・52-61:
0〜9 - ・62:
+ - ・63:
/ - ・パディング(特別):
=
合計65文字のうち64文字がデータ、1文字(=)がパディング専用。これがRFC 4648 Section 4 の「A 65-character subset of US-ASCII is used」の意味です。
3. パディング「=」の役割(Section 3.2)
3で割り切れない長さの入力を処理する仕組み:
- ・入力が3バイト:4文字(パディングなし)
- ・入力が2バイト:3文字+
=(パディング1個) - ・入力が1バイト:2文字+
==(パディング2個)
RFC 4648 Section 3.5は「pad bitsは0でなければならない」と規定。たとえば1バイト入力時、最後の6ビットのうち先頭2ビットだけが入力データで、残り4ビットは0で埋めるという意味。これが守られないと、同じデータから複数の異なるBase64文字列が生成されてしまい、canonical(正規化された)形でなくなります。
4. JavaScript btoa/atob の制約
ブラウザのbtoa(s)は文字列sを「各文字が1オクテット(Latin1範囲)」とみなしてBase64化します。日本語の「あ」(UTF-16のコードユニットで0x3042)を渡すと、コードユニットがLatin1範囲を超えているのでInvalidCharacterErrorが投げられます。
つまりbtoa("あ")は失敗する。btoa(unescape(encodeURIComponent("あ")))という古いハックはあるけれど、現代ではTextEncoder/TextDecoderを使うのが正解です。
5. UTF-8対応のBase64エンコード実装
手軽屋のツールが採用しているコード(要旨):
// UTF-8でバイト列に → 8bit文字列に → btoa
const b64encode = (s) => {
const bytes = new TextEncoder().encode(s);
let bin = "";
bytes.forEach((b) => (bin += String.fromCharCode(b)));
return btoa(bin);
};
// atob → Uint8Array → TextDecoder("utf-8")
const b64decode = (s) => {
const bin = atob(s.replace(/\s/g, ""));
const bytes = Uint8Array.from(bin, (c) => c.charCodeAt(0));
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
};ポイント:(1) TextEncoderはUTF-8固定。(2) 8bit文字列の中継はLatin1範囲だけでASCII以外もbtoaが受理する条件を満たすため。(3) decode側でfatal: trueを指定すると、壊れたバイト列のときに例外を投げてくれる。
6. Base64URLとの違い(RFC 4648 Section 5)
URLやファイル名に直接埋め込むときは「Base64URL」(RFC 4648 Section 5)を使います。通常のBase64との違い:
- ・
+→-(マイナス) - ・
/→_(アンダースコア) - ・パディング
=は省略可能(仕様上はMAY)
JWT(JSON Web Token)・OIDC・データURI・ファイル名など、URL系の文脈ではBase64URLが標準。本ツールの出力からBase64URLが必要なときは、置換で対応してください:
const toBase64URL = (s) =>
s.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");7. 折り返し(76文字制限)の罠
MIME(メール本文の符号化)では、Base64出力を76文字ごとに改行で折り返す慣習があります。これは送信プロトコル(SMTP)の行長制限が由来。RFC 4648 Section 3.1 は「明示的に指示されない限り改行を入れてはならない(MUST NOT)」と規定。
そのため、URLやDB列にBase64を保存するときは折り返さない方が安全。メール用途なら別途76文字折り返しが必要。本ツールは折り返しなしを採用(RFC 4648準拠)。
8. 関連ツール・記事
- ・URLエンコード・Base64変換:UTF-8対応のBase64実装
- ・パーセントエンコーディング仕様詳解:RFC 3986とWHATWG URL Standard
- ・JSONフォーマッタ:JWT本体のJSONを整形
- ・パスワード生成:APIキー類の生成