Base64デコードで日本語が文字化けする原因と対処
btoa("こんにちは") が InvalidCharacterError を出したり、 一見成功したのに復号すると "ã" だらけになる——UTF-8 を絡めた Base64 で頻出の罠です。 原因は btoa/atob が Latin-1 限定であること。 本記事では UTF-8 安全なコードと、Base64URL の扱いをまとめます。
btoa/atob only accept Latin-1 code points, so UTF-8 multi-byte strings get mangled. Use TextEncoder/TextDecoder for a safe round-trip.
TL;DR
- ブラウザの
btoa/atobは Latin-1 専用。UTF-8 は別途エンコードが必要 - UTF-8 安全な方法:
TextEncoder→ バイト列 →btoa - JWT の Base64URL は
-_→+/置換 +=パディングが必要
1. UTF-8 安全な JavaScript 実装 / UTF-8 safe JavaScript
// Encode (string → Base64)
function utf8ToBase64(s) {
const bytes = new TextEncoder().encode(s);
let bin = "";
bytes.forEach(b => (bin += String.fromCharCode(b)));
return btoa(bin);
}
// Decode (Base64 → string)
function base64ToUtf8(b64) {
const bin = atob(b64);
const bytes = Uint8Array.from(bin, c => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
utf8ToBase64("こんにちは"); // "44GT44KT44Gr44Gh44Gv"
base64ToUtf8("44GT44KT44Gr44Gh44Gv"); // "こんにちは"2. Base64URL 対応 / Base64URL
JWT や OAuth 関連の Base64URL は URL安全文字セットを使い、末尾パディングを省略します。
function base64UrlDecode(s) {
const b64 = s.replace(/-/g, "+").replace(/_/g, "/");
const pad = b64.length % 4;
const padded = pad ? b64 + "=".repeat(4 - pad) : b64;
return base64ToUtf8(padded);
}3. 言語別の正しい書き方 / Other languages
Python
import base64
b64 = base64.b64encode("こんにちは".encode("utf-8")).decode("ascii")
text = base64.b64decode(b64).decode("utf-8")Shell
echo -n "こんにちは" | base64 # macOS の base64 は -w 0 不要
echo "44GT44KT44Gr44Gh44Gv" | base64 -d4. よくあるハマり / Common pitfalls
- メール添付の Base64 は 76 文字ごとに改行。デコード前に
/\s/gで除去 - 画像の Data URI に
data:image/png;base64,のプレフィクスを付け忘れ - Windows の
certutilは CRLF を挿入する点に注意
5. Node.jsでの簡単な書き方 / The simple way in Node.js
ブラウザにはBufferがないためTextEncoderを経由する必要がありますが、 Node.jsはBufferが文字エンコーディングを直接扱えるため、はるかに簡潔に書けます。
// Encode
const b64 = Buffer.from("こんにちは", "utf-8").toString("base64");
// Decode
const text = Buffer.from(b64, "base64").toString("utf-8");ブラウザ向けのTextEncoder/TextDecoderによるバイト列変換は、まさにNode.jsのBufferが内部で行っていることを手動で再現しているものです。
6. 新しいネイティブAPI: Uint8Array.fromBase64 / toBase64
TC39提案(Uint8ArrayのBase64/Hex変換)がECMAScriptに採用され、対応ブラウザ・Node.jsではbtoa/atobを介さずBase64を直接扱えます。UTF-8のバイト列を扱う分には Latin-1制限の問題が最初から発生しません。
// 対応環境(要フィーチャーチェック)
const bytes = new TextEncoder().encode("こんにちは");
const b64 = bytes.toBase64(); // Uint8Array -> Base64文字列
const decoded = Uint8Array.fromBase64(b64);
new TextDecoder().decode(decoded); // "こんにちは"
// 対応チェック
const supportsNativeBase64 = typeof Uint8Array.prototype.toBase64 === "function";比較的新しいAPIのため、幅広い環境をサポートする必要がある場合は本記事冒頭のTextEncoder+btoa方式か、Buffer(Node.js)の利用を推奨します。
7. 環境別・正しい変換方法の早見表 / Correct method by environment
| 環境 / Environment | UTF-8安全な方法 |
|---|---|
| ブラウザ(広く対応させたい場合) | TextEncoder → String.fromCharCode → btoa |
| ブラウザ(新しめの環境限定でよい場合) | Uint8Array.prototype.toBase64/fromBase64 |
| Node.js | Buffer.from(str, "utf-8").toString("base64") |
| Python | base64.b64encode(s.encode("utf-8")) |
直接 btoa("日本語") | ❌ InvalidCharacterErrorまたは文字化け |
8. English summary
Browser btoa/atob only handle Latin-1 code points, so passing Japanese (UTF-8) strings directly produces garbage or throws InvalidCharacterError. The safe path is: TextEncoder to get UTF-8 bytes, map bytes to a Latin-1 string viaString.fromCharCode, then btoa. Reverse the process for decoding. JWT segments use Base64URL: replace -_ with +/ and pad with =before decoding. Strip whitespace from MIME-wrapped Base64 first.
よくある質問 / FAQ
- btoa と atob はなぜ日本語で壊れるのか?
- btoa/atob は Latin-1(0-255)のコードポイントのみ対応します。UTF-8 のマルチバイト列をそのまま渡すとバイナリが崩れます。TextEncoder/TextDecoder で UTF-8 を経由してください。
- Why do btoa/atob break on Japanese?
- They handle only Latin-1 code points. Go through TextEncoder/TextDecoder to round-trip UTF-8 correctly.
- JWT の Base64URL で失敗する
- 標準Base64と異なり -_ を使い、末尾の = を省略します。-/_ を +// に戻し、長さを4の倍数にパディングしてからデコードしてください。