← 記事一覧

Base64 は暗号化ではない:何を隠せて何を隠せないのか

Base64 はエンコードで暗号化ではない。パスワード、JWT ペイロード、API キー、Basic 認証ヘッダ、URL データ、ログが読める理由。

「保存する前に Base64 かけとけばいい」——設定ファイル、社内ダッシュボード、ブラウザバンドル、Webhook ハンドラ、認証コードを十分にレビューしていれば、いつかはこのセリフを目にします。出力がごちゃごちゃに見えるので、それらしく聞こえるんですね。

c2VjcmV0IHBhc3N3b3Jk

しかし Base64 は暗号化ではありません。パスワード保護でもありません。API キー、JWT クレーム、ユーザー ID、Basic 認証の資格情報、URL 内の非公開データを安全に隠す手段でもありません。同じ値は 1 回の呼び出しでデコードされます。

atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"

現場のルールはシンプルです。秘密鍵なしで元の値が戻ってくるなら、Base64 は保護していない。表現を変えただけです。

セキュリティのクイックテスト

コード内で Base64 を見たら、何の仕事をしているのか自問しましょう。

Base64 が使われている目的 妥当か? 確認すべきこと
バイト列を JSON に載せる Yes サイズの上限は妥当に
小さな data: URI を埋め込む Yes 大きなファイルを無条件でインライン化しない
暗号文、nonce、ソルト、署名バイトをテキストで保存 Yes 暗号処理は Base64 の前に
不透明なページネーションカーソルを運ぶ 通常 OK インタフェースの詳細として扱う、アクセス制御ではない
Basic 認証ヘッダを組み立てる HTTPS 併用時のみ リクエストを守るのは TLS、Base64 ではない
フロントエンドで API キーを隠す No 値もデコーダも全ブラウザに配られる
パスワードを保存する No 遅いパスワードハッシュを使う
JWT クレームを隠す No 署名済み JWT のペイロードは JWE 化しない限り読める
URL でユーザー ID やロールを隠す No サーバー側の認可は必ず走らせる

頭の中の文が「誰も読めないように」なら、それは Base64 の話ではありません。暗号化、ハッシュ、署名、アクセス制御、シークレット管理の話です。

Base64 が実際に行うこと

Base64 はバイナリからテキストへのエンコーディング方式です。RFC 4648 は一般的な Base64 アルファベットをこう定義します。

A-Z a-z 0-9 + / =

= はパディングです。Base64 は入力バイトを 24 ビット単位でまとめ、3 バイトを 4 つの印刷可能文字にします。だから Base64 の出力は、圧縮前の生バイトよりおよそ 33 % 大きくなります。

btoa("hello");
// "aGVsbG8="

atob("aGVsbG8=");
// "hello"

同じ往復はターミナルからも動きます。

printf '%s' 'secret password' | base64
# c2VjcmV0IHBhc3N3b3Jk

printf '%s' 'c2VjcmV0IHBhc3N3b3Jk' | base64 -d
# secret password

パスワードプロンプトも、秘密鍵も、復号ステップも、ソルトも、nonce も、IV も、タグも、ワークファクタもありません。エンコードされた文字列を持っている人なら誰でも戻せます。

Base64 と Base64url

標準 Base64 は URL 安全とは限りません。+/= は URL、ファイル名、Cookie、トークンセグメントで扱いにくいからです。Base64url はアルファベットを変えます。

特徴 標準 Base64 Base64url
62 番目と 63 番目の文字 +/ -_
パディング 通常 = しばしば省略
よく使う場所 MIME、PEM、Basic 認証、バイナリフィールド JWT、URL トークン、ファイル名
セキュリティ上の差 なし なし

これは JWT のデバッグで効いてきます。JWT はパディング付きの標準 Base64 ではなく、Base64url を使います。

header.payload.signature

JWT のセグメントを標準 Base64 デコーダに貼って失敗するとき、その文字列は必ずしも秘密でも暗号化されているわけでもありません。単に Base64url として扱う必要があるだけかもしれません。

function base64urlToBase64(part) {
  let out = part.replace(/-/g, "+").replace(/_/g, "/");
  while (out.length % 4 !== 0) out += "=";
  return out;
}

この変換はアルファベットを変えるだけです。機密性を付け足したりはしません。

UTF-8:ブラウザの落とし穴

ブラウザの btoa()atob() はバイナリ文字列を扱います。純粋な ASCII の例なら問題ありません。

btoa("hello");
// "aGVsbG8="

しかし Unicode テキストにはバイト化のステップが先に要ります。これは hello でテストしてから、顧客名、中国語、絵文字、アラビア語をエンコードして初めて気づく、というパターンでチームを驚かせます。

btoa("你好");
// InvalidCharacterError in browsers

UTF-8 バイトを使いましょう。

function utf8ToBase64(text) {
  const bytes = new TextEncoder().encode(text);
  const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
  return btoa(binary);
}

function base64ToUtf8(base64) {
  const binary = atob(base64);
  const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
  return new TextDecoder().decode(bytes);
}

const encoded = utf8ToBase64("你好");
base64ToUtf8(encoded);
// "你好"

このサイトの Base64 Encode & Decode ツールはブラウザ内で UTF-8 の往復を処理するので、非 ASCII テキストは文字化けせずにテキストとして戻ります。

なぜ Base64 と暗号化を混同するのか

Base64 は本物のセキュリティのすぐ隣によく現れます。

  • TLS 証明書は PEM テキストで保存される。
  • JWT は Base64url のセクションを使う。
  • HTTP Basic 認証は username:password に Base64 を使う。
  • SSH 公開鍵は Base64 でエンコードされた鍵素材を含む。
  • 暗号文はしばしば保存や転送の前に Base64 でエンコードされる。

最後の点が一番の混乱源です。暗号化データが Base64 でエンコードされていることはありますが、Base64 は暗号化ではありません。セキュリティは AES-GCM、XChaCha20-Poly1305、RSA-OAEP、JWE、TLS のような暗号処理から来ています。Base64 はその結果のバイト列を JSON、メール、ヘッダ、DB カラムに載せやすくしただけです。

コードでも製品文言でも、正確な言葉を使いましょう。

encodedBackup      // 単に Base64 なら OK
encryptedBackup    // 本物の暗号化を行った場合のみ OK
signedState        // HMAC や署名で完全性を保護しているなら OK
passwordHash       // パスワードハッシュアルゴリズムで生成したなら OK

ここでの名前は装飾ではありません。誤解を招く名前があると、後のレビュアーがセキュリティ境界の存在を仮定してしまいます。実際にはないのに。

エンコーディング vs 暗号化 vs ハッシュ vs 署名

これらの操作は別の問題を解きます。

操作 主な目的 秘密が必要? 可逆?
エンコーディング 表現を変える 不要 可能 Base64、Base64url、URL エンコード
暗号化 平文を隠す 必要 鍵があれば可能 AES-GCM、XChaCha20-Poly1305、JWE
ハッシュ 一方向の指紋 不要 不可 チェックサム用の SHA-256
パスワードハッシュ 遅いパスワード検証 ソルト+コスト設定 不可 Argon2id、bcrypt、scrypt
署名 / MAC 完全性と出所を証明 必要、または鍵ペア 不可 HMAC、JWS、Ed25519 署名

Base64 が答えるのは「このバイト列はテキストチャネルを通れるか?」です。「誰が読めるか?」「誰が書き換えたか?」「このユーザーはそのレコードにアクセスしていいか?」には答えません。

実在するセキュリティミス 1:フロントエンドコードに隠した API キー

このパターンは React アプリ、ブラウザ拡張、WordPress テーマ、社内管理ツールで見かけます。

// 結局、鍵は全ブラウザに配られる。
const apiKey = atob("c2stbGl2ZS1hYmMxMjM0NTY3ODk=");

fetch("https://api.example.com/report", {
  headers: { Authorization: `Bearer ${apiKey}` },
});

Base64 は助けになりません。ブラウザはエンコードされた値もデコーダも受け取っています。誰でも DevTools を開き、JavaScript バンドルを覗き、同じ atob() を実行して鍵をコピーできます。

修正は設計レベルです。

  • 本物のシークレットはサーバー側に置く。
  • ブラウザに置く鍵はスコープとドメイン制限を狭くする。
  • センシティブな上流呼び出しはバックエンド経由でプロキシする。
  • すでにユーザーに配ってしまったシークレットはローテートする。
  • シークレットスキャナは Base64 風の文字列もデコードできる前提で考える。

鍵をエンコードするとコードレビューは面倒になります。鍵は安全になりません。

実在するセキュリティミス 2:Base64 で保存されたパスワード

これは長年 DB に静かに居座り得るので厄介です。

// 可逆な保存。やってはいけない。
const storedPassword = btoa(password);

// 後で:
const password = atob(storedPassword);

DB が漏れたら、パスワードはその場で全部戻ります。破るコストはゼロ、破るものが何もないので。

パスワードの保存は一方向であるべきです。

// パスワードハッシュライブラリを使ったフローの形。
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, candidatePassword);

通常のログインシステムでユーザーのパスワードを復元する必要はまずありません。候補パスワードを保存済みハッシュと照合できれば十分です。

実在するセキュリティミス 3:Basic 認証を暗号化と誤解する

このヘッダは人を騙せる程度にエンコードされて見えます。

Authorization: Basic dXNlcjpwYXNzd29yZA==

デコード結果は次のとおり。

atob("dXNlcjpwYXNzd29yZA==");
// "user:password"

HTTP Basic 認証はリクエストが HTTPS で守られている場合に限り許容されます。TLS が HTTP のやりとりを転送中に暗号化してくれる。Base64 は username:password をヘッダに載せられる ASCII 文字列に詰め込むだけです。

平文 HTTP 上ではネットワーク観察者が資格情報を読めます。ログ、リバースプロキシ、APM トレース、サポート用ダンプでは、Basic 認証ヘッダを平文パスワードと同じ扱いにしましょう。

実在するセキュリティミス 4:JWT ペイロードを非公開と見なす

普通の署名済み JWT は 3 つの Base64url セクションを持ちます。

header.payload.signature

最初の 2 つは JSON にデコードされます。3 つ目が署名です。

eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0

デコードするとこう。

{
  "sub": "1234",
  "role": "admin"
}

これはトークンが偽物という意味ではありません。署名済み JWT は改ざん検知が効いていて、なお読める、という状態が普通です。署名が守るのは完全性です。サーバーに、そのトークンが信頼できる発行者のものか、ヘッダやペイロードが書き換わっていないかを教える。ペイロードは隠しません。

普通の署名済み JWT のペイロードにパスワード、API キー、セッションシークレット、完全なクレジットカード番号、医療情報、非公開のプロフィールデータを入れてはいけません。クレームに機密性が必要なら JWE を使うか、そもそもトークンに入れないことです。

もう一つ。デコードは検証ではありません。開発者向けツールはクレームを表示できますが、サーバーは信頼判断の前に、署名、issuer、audience、有効期限、key ID、アルゴリズムポリシー、アプリ固有の認可ルールを検証しなければなりません。

実在するセキュリティミス 5:不透明そうな URL パラメータ

レガシーなアプリは状態をこう渡しがちです。

/profile?data=eyJ1c2VySWQiOjQyfQ==

デコードするとこう。

{
  "userId": 42
}

問題は 2 つ生えます。誰でも読める。誰でも書き換えて再エンコードできる。もし /profile?data=... だけがどのユーザーレコードをロードするかを決めているなら、バグは認可の欠落です。

より安全なパターン。

  • デコードした URL データは信頼できない入力として扱う。
  • 権限はサーバーで再チェックする。
  • 状態が機密なら、短いランダムなサーバー側参照を優先する。
  • クライアントに改ざん検知付きの状態を持たせるなら HMAC か署名を使う。
  • 非公開の状態は URL に入れない。URL はログ、ブラウザ履歴、アナリティクス、サポートのスクリーンショットにコピーされる。

実在するセキュリティミス 6:「暗号化」エクスポートが実は単なるエンコード

コードレビューのもう一つのにおいはこんな関数です。

function exportEncryptedBackup(data) {
  return btoa(JSON.stringify(data));
}

関数名は暗号化と言っている。コードはエンコードだと言っている。

エクスポートが持ち運べるテキストブロブでいいだけなら、リネームしましょう。

function exportBase64Backup(data) {
  return btoa(JSON.stringify(data));
}

ユーザーにエクスポートが暗号化されていると伝えているなら、本物の認証付き暗号化を使い、鍵の扱いを明示しましょう。監査済みの暗号ライブラリ、ランダムな nonce/IV、認証タグ、安全な鍵生成、鍵の保管計画。Base64 はその後で暗号文を包むために使えます。

Base64 が本当に得意なこと

Base64 は有用です。間違いはそれにセキュリティの仕事を割り当てることです。

いい使い方。

  • JSON、XML、HTML、メール内のバイナリデータ。
  • MIME メールの添付。
  • 画像やフォントのための小さな data: URI。
  • 暗号文、署名、nonce、ソルト、公開鍵など暗号出力のフォーマット。
  • JWT や JOSE のコンパクトシリアライゼーションのセグメント。
  • 可読性が製品上の関心事で、セキュリティ境界ではない不透明なページネーションカーソル。
  • API フィールド、JWT ペイロード、設定ブロブのデバッグ。

安全な言い回しは「Base64 エンコード済み」であって「暗号化済み」ではありません。この一語で、驚くほど多くの本番トラブルが避けられます。

代わりに何を使うか

セキュリティが欲しくて Base64 に手を伸ばしたなら、仕事に合う原始関数を選びましょう。

目的 代わりに使うもの
データをユーザーや攻撃者から隠す AES-GCM や XChaCha20-Poly1305 のような認証付き暗号化
転送中のデータを保護 HTTPS/TLS
パスワードを保存 Argon2id、bcrypt、または scrypt、パスワードごとのソルトと調整済みコスト設定で
API キーを保存 シークレットマネージャ、サーバー側環境、スコープ、監査ログ、ローテーション
値が改ざんされていないことを証明 HMAC またはデジタル署名
URL 状態をユーザーに書き換えられないようにする サーバー側認可+署名付き状態、またはサーバー側セッション参照
JWT クレームを非公開に保つ JWE、または非公開クレームをそもそもトークンに入れない
JSON、メール、ヘッダにバイナリを入れる Base64 で正解

難しいのはたいていアルゴリズム名ではありません。脅威モデリング、鍵管理、ローテーション、アクセス制御、そもそも誰が平文を見られるべきかの判断です。

実用的なコードレビュー用チェックリスト

Base64 の使用箇所を探しましょう。

rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"

ヒットごとに分類します。

  1. 転送:JSON、MIME、data URI、PEM、ストレージ向けのバイト→テキスト変換。通常は問題なし。
  2. 暗号後のフォーマット:暗号文、nonce、署名、ソルト、公開鍵。暗号処理が正しければ通常は問題なし。
  3. 不透明なインタフェース:カーソル、招待コード、クライアントに運ばせる状態。認可と改ざん保護を確認。
  4. シークレット隠し:API キー、パスワード、トークン、資格情報、非公開クレーム、ユーザーデータ。これはダメ。
  5. 誤解を招く命名:変数や UI 文言が暗号化、安全、保護、秘密、マスク、隠しなどを名乗る。名前か実装のどちらかを直す。

続けてこう問いましょう。

  • ブラウザ、モバイルアプリ、ユーザー、ログコレクタ、プロキシ、サポートツールがエンコード済みの値を受け取り得るか?
  • そのすぐ隣にデコーダがあるか?
  • デコード後にサーバーが権限を検証するか?
  • クライアントが編集できる値なら署名されているか?
  • JWT なら、信頼判断の前に署名を検証しているか?
  • パスワードなら、なぜそもそも可逆なのか?

これでレビューは地に足がついたままになります。data URI の PNG は退屈です。JavaScript バンドル内の本番 API キーは退屈ではありません。

インシデント対応:Base64 の「シークレット」を見つけたら

Base64 エンコードされたシークレットをソースコード、ログ、チケット、アナリティクス、URL、DB エクスポートで見つけたら、こう動きます。

  1. 手元でデコードして中身を確認。
  2. デコードした値は露出したものとして扱う。
  3. 見えた可能性がある資格情報、トークン、鍵をローテートする。
  4. 可能な範囲でソース、ログ、スクリーンショット、チケットからエンコード済みコピーを取り除く。
  5. 設計をサーバー側シークレット、スコープ付き公開鍵、署名付き状態、本物の暗号化、パスワードハッシュに置き換える。
  6. 同じパターンが再発しないようレビュー規則を追加する。

その Base64 文字列が「気づきにくかった」かどうかで議論に時間を使う必要はありません。自動スキャナ、ブラウザのユーザー、攻撃者、みんなデコードできます。

本当に暗号化トークンが必要になったとき

標準的な署名済み JWT は JWS を使います。読めるペイロード、改ざん検知可能な署名。クレームに本当に機密性が要るなら JWE(JSON Web Encryption)を使いましょう。

コンパクト JWE は 5 つの Base64url セクションを持ちます。

protected-header.encrypted-key.iv.ciphertext.authentication-tag

これらのセクションは相変わらず Base64url テキストです。機密性は JWE の暗号化アルゴリズムと受信者の鍵から来るのであって、Base64url そのものから来るのではありません。

多くの Web アプリでは、もっとシンプルな設計のほうが良いです。センシティブな状態はサーバー側に置き、ブラウザには短いランダムなセッション ID を渡す。読める JWT を全部 JWE にする必要はありません。

よくある質問

Base64 は暗号化ですか?

いいえ。Base64 は秘密鍵を持たない可逆なエンコーディングです。文字列を持っている人は誰でも、ブラウザ関数、ターミナルコマンド、オンラインツール、あるいは数行のコードでデコードできます。

せめて Base64 は難読化にはなりますか?

もっとも弱い意味でだけです。ぱっと見の値は隠せるかもしれませんが、開発者、攻撃者、スキャナ、ブラウザ拡張、ログリーダー、サポートツールを止めることはできません。

パスワードや API キーに Base64 は安全ですか?

いいえ。パスワードには Argon2id、bcrypt、scrypt のような遅いパスワードハッシュが要ります。API キーはサーバー側か、スコープ・監査可能性・ローテーションを備えたシークレットマネージャに置いておくものです。

誰でも JWT のペイロードを読めますか?

はい、普通の署名済み JWT なら読めます。ヘッダとペイロードは Base64url でエンコードされた JSON です。署名は検証すれば完全性を証明できますが、クレームは隠しません。

安全でないなら、なぜ Basic 認証は Base64 を使うのですか?

Basic 認証は username:password を HTTP ヘッダに載せるために Base64 を使います。転送時の暗号化は HTTPS/TLS が担います。HTTP のままなら Basic 認証の資格情報はネットワーク上で事実上平文です。

Base64url は Base64 より安全ですか?

いいえ。Base64url は +/-_ に置き換え、URL や JWT で使えるようにパディングを省略することがよくあります。それでも可逆なエンコーディングです。

暗号化のためには Base64 の代わりに何を使えばいいですか?

AES-GCM、XChaCha20-Poly1305、JWE のような監査済みの認証付き暗号化に、適切な鍵生成、鍵保管、ローテーション、アクセス制御を組み合わせて使います。自作の暗号フォーマットは設計しないこと。

そもそも Base64 は何のためのものですか?

Base64 はバイトをテキストとして表現するものです。JSON のバイナリフィールド、メール添付、data URI、PEM ファイル、署名、暗号文のフォーマット、URL 安全なトークンセグメント。ラッパーであってセキュリティ境界ではありません。

ブラウザ内でエンコード・デコード

いま Base64 文字列を確認したい? fixjson.org の Base64 Encode & Decode は、ブラウザ内でローカルにテキスト(UTF-8 コンテンツを含む)をエンコード・デコードできます。JWT ペイロード、API レスポンスのフィールド、data: URI の確認に使ってください。ただし、デコードを検証や復号と取り違えないように。

関連ツール&ガイド

出典

  • RFC 4648 — Base64、Base64url、パディング、アルファベット、セキュリティ考慮。
  • RFC 7617 — HTTP Basic 認証とその Base64 資格情報フォーマット。
  • RFC 7519 — JWT の構造、信頼判断、プライバシー考慮。
  • RFC 7516 — JSON Web Encryption とコンパクト JWE シリアライゼーション。
  • MDN btoaMDN atob — ブラウザの Base64 プリミティブと UTF-8 の注意点。
  • OWASP Cryptographic Storage Cheat Sheet — 暗号化、認証付きモード、鍵管理。
  • OWASP Password Storage Cheat Sheet — パスワードハッシュのガイダンス。

最終確認:2026 年 7 月。