0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Base64とBase64URL — Node.js・ブラウザでのエンコード/デコード早わかり

0
Posted at

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をブラウザでデコードする関数

src/decodeBase64Url.ts
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への変換

src/toBase64Url.ts
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デコードすれば誰でも読める。「署名されている=中身が秘密」ではない。

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?