Base64・Base64URLは「知ってるつもり」で雰囲気で使いがち。
パディングの仕組み、ブラウザで日本語デコードするときの落とし穴、Node.jsとの差異をまとめた。
Base64とは
文字列・画像・ファイルなどのバイナリデータを、ASCII安全な文字列に変換するエンコード方式。
hello → aGVsbG8=
日本語 → 5pel5pys6Kqe
使いどころはこのあたり。
- メールの添付ファイル(MIME)
- JSONやREST APIでバイナリを渡す
- Data URLによる画像埋め込み
- Basic認証のCredentials
- JWTのヘッダー・ペイロード
Base64は暗号化ではない。変換ルールは公開されており、誰でもデコードできる。パスワードやAPIキーの保護には使えない。パスワード保存にはbcrypt/Argon2、秘密情報の保護には暗号化+鍵管理を使う。
使われる文字とパディング =
Base64のアルファベットは64文字+パディング用の =。
A-Z a-z 0-9 + /
元データを3バイトずつ取り出し、4文字のBase64に変換する。元データが3の倍数バイトでない場合、末尾に = を足して4文字に揃える。
| 元データ長 % 3 | パディング |
|---|---|
| 0 | なし |
| 1 | == |
| 2 | = |
| 入力 | 文字数 | バイト数 | バイト数 % 3 | Base64 | パディング |
|---|---|---|---|---|---|
"h" |
1 | 1 | 1 | aA== |
== |
"hi" |
2 | 2 | 2 | aGk= |
= |
"hey" |
3 | 3 | 0 | aGV5 |
なし |
パディングを決めるのはバイト数であって文字数ではない。ASCIIは1文字=1バイトなので上の例では一致しているが、UTF-8の日本語は1文字=3バイトになるため一致しない。
| 入力 | 文字数 | バイト数 | バイト数 % 3 | Base64 | パディング |
|---|---|---|---|---|---|
"日" |
1 | 3 | 0 | 5pel |
なし |
"日本" |
2 | 6 | 0 | 5pel5pys |
なし |
"A日" |
2 | 4 | 1 | QeaXpQ== |
== |
"日本" は2文字だがUTF-8で6バイト(3の倍数)なのでパディングなし。"A日" は同じ2文字でも4バイト(余り1)なので == が付く。
Base64URLとは
URL・ファイル名で安全に使えるようBase64を微調整した変種。
| Base64 | Base64URL | |
|---|---|---|
| 62番目 | + |
- |
| 63番目 | / |
_ |
| パディング |
= あり |
省略が一般的 |
Base64: abc+def/ghi==
Base64URL: abc-def_ghi
JWTは . 区切りでBase64URL文字列を3つ並べた形式。ヘッダーとペイロードはデコードすれば中身が読めるので、秘密情報を入れてはいけない。
Node.jsでのエンコード/デコード
Node.jsは Buffer がすべてやってくれる。
Base64
const text = "日本語";
// エンコード
const encoded = Buffer.from(text, "utf8").toString("base64");
console.log(encoded);
5pel5pys6Kqe
// デコード
const decoded = Buffer.from("5pel5pys6Kqe", "base64").toString("utf8");
console.log(decoded);
日本語
Base64URL
"base64url" を指定するだけ。パディングは自動で省略される。
const text = "日本語 "; // 末尾に半角スペース(計10バイト)
console.log(Buffer.from(text, "utf8").toString("base64"));
// 5pel5pys6KqeIA==
console.log(Buffer.from(text, "utf8").toString("base64url"));
// 5pel5pys6KqeIA
デコードも同様。
const decoded = Buffer.from("5pel5pys6KqeIA", "base64url").toString("utf8");
console.log(decoded);
日本語
ブラウザでのデコード
ブラウザには Buffer がない。代わりに atob() + TextDecoder を組み合わせる。
ASCII文字だけなら atob() で十分
const result = atob("aGVsbG8=");
console.log(result); // "hello"
hello はUTF-8で1文字=1バイトなので、atob() の戻り値がそのまま読める。
日本語は atob() だけでは壊れる
atob() は「Base64をバイト列に戻して、各バイトを String.fromCharCode で文字にしたもの」を返す。UTF-8でマルチバイトな日本語は、この段階では意味のない文字化け文字列になる。
TextDecoder でUTF-8として再解釈して初めて日本語に戻る。
Base64URLをブラウザでデコードする関数
export const decodeBase64Url = (data: string): string => {
const base64 = toStandardBase64(data);
// Node.js環境ならBufferで済む
if (typeof Buffer !== "undefined") {
return Buffer.from(base64, "base64").toString("utf8");
}
// ブラウザ: atob → Uint8Array → TextDecoder
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
};
const toStandardBase64 = (data: string): string => {
const base64 = data.replace(/-/g, "+").replace(/_/g, "/");
return base64 + "=".repeat((4 - (base64.length % 4)) % 4);
};
各ステップでの変数の中身を追うとこうなる。
入力: "5pel5pys6KqeIA" ← Base64URL(=省略)
toStandard: "5pel5pys6KqeIA==" ← 通常のBase64に復元
atob(): "æ\x97¥æ\x9C¬è\xAA\x9E " ← 各charがUTF-8の1バイトに対応
Uint8Array: [230,151,165, 230,156,172, 232,170,158, 32]
^^^日^^^ ^^^本^^^ ^^^語^^^ ^スペース
TextDecoder: "日本語 "
逆方向: Base64 → Base64URLへの変換
export const toBase64Url = (base64: string): string =>
base64
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
やっていることは3つだけ。+ → -、/ → _、末尾の = を削除。
Node.js vs ブラウザ: API対応表
| やりたいこと | Node.js | ブラウザ |
|---|---|---|
| Base64エンコード | Buffer.from(str, "utf8").toString("base64") |
btoa() — ただしASCIIのみ |
| Base64デコード | Buffer.from(b64, "base64").toString("utf8") |
atob() + Uint8Array + TextDecoder
|
| Base64URLエンコード | Buffer.from(str, "utf8").toString("base64url") |
btoa() → 記号置換 + = 除去 |
| Base64URLデコード | Buffer.from(b64url, "base64url").toString("utf8") |
記号復元 + = 補完 → atob() → TextDecoder
|
Node.jsは Buffer 一発で終わる。ブラウザは atob()/btoa() がバイト列⇔ASCII文字列の変換しかしないため、マルチバイト文字を扱うには TextEncoder/TextDecoder との組み合わせが必須。
Base64とBase64URLの使い分け
| 用途 | 方式 | 理由 |
|---|---|---|
| Data URL | Base64 | 仕様で + / = が許容される |
| メール添付(MIME) | Base64 | RFC 2045準拠 |
| JWT | Base64URL |
. 区切りでURLに載せるため |
| URLクエリパラメータ | Base64URL |
+ / = がURLで特殊文字になる |
| Cookie | Base64URL |
= がCookieの区切り文字と衝突する |
| 署名対象データ | Base64URL | 正規化の安定性が必要 |
JWTのヘッダーとペイロードはBase64URLデコードすれば誰でも読める。「署名されている=中身が秘密」ではない。