使うことと理解することのギャップ
キャリアの初期、認証の問題をデバッグしていたとき、シニア開発者が「JWTが期限切れじゃないか?」と聞いた。
確認したと答えた。認めなかったこと:ウェブサイトにトークンを貼り付けて有効期限のタイムスタンプを見たが、見ているものを完全には理解していなかった。安全なのか?誰かがこのトークンを読めるのか?暗号化されているのか?「署名」されていて「安全」とされているのに、なぜどのウェブサイトでもデコードできるのか?
このガイドはそのころの自分のために書いた。Webエンジニアリングで常に出てくる3つの概念、ほとんどのチュートリアルがすでに知っているものとして前提にしている、実は説明されれば単純な概念。
Base64:暗号化でも圧縮でもなく、ただの翻訳。
まず最初に知るべき最重要事項:Base64はセキュリティメカニズムではない。データを隠さない。誰でも即座にデコードできる。「エンコードされているように見えるからURLに入れても安全だろう」と思ったことがあるなら——安全ではない。
Base64は印刷可能なASCII文字のみを使ってバイナリデータを表現する方法だ。それだけ。存在する実用的な理由:バイナリデータはものを壊す。メール、HTTPヘッダー、URL、HTML属性——これらのシステムはテキスト用に設計された。ヌルバイトと制御文字を持つ生のバイナリデータは、テキストベースのシステムを通過するときに破損したり変更されたりする可能性がある。
Base64はバイナリを64文字の安全なアルファベットに変換する:A〜Z、a〜z、0〜9、+と/。出力は入力より約33%大きくなるが、どのテキストシステムを通っても無傷で届くことが保証される。
実際にどこで見かけるか
HTTP基本認証:ユーザー名:パスワードで認証するとき、認証情報はBase64エンコードされてAuthorization: Basicヘッダーで送信される。だから平文HTTP上のBasic Authは危険だ——リクエストを傍受した誰でも認証情報を即座にデコードできる。HTTPSは必須だ。
JWTトークン:JWTトークンの3つのセクションはそれぞれBase64urlエンコードされている(URLで安全にするために+と/を-と_に置き換えた変種)。
Data URI:<img src="data:image/png;base64,iVBORw0KGgo...">——別のHTTPリクエストを作成する代わりに画像データをHTMLやCSSに直接埋め込む。
メール添付ファイル:MIMEエンコーディングは、テキストのみに設計されたメールサーバーを通じてバイナリファイルが転送できるようにBase64を使う。
自分で試してみよう:Base64エンコーダーに任意のテキストを貼り付けて結果を見よう。エンコードされた文字列を貼り直してデコードしよう。「エンコード」版は何も隠していない——ただ別のアルファベットを使っているだけだ。
JWT:3つのBase64文字列と1つの署名
JSON Webトークンはこのように見える:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiIxMjMiLCJlbWFpbCI6InVzZXJAZXhhbXBsZS5jb20iLCJleHAiOjE3MTM5MzYwMDB9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
ちょうど2つのドット。3つのセクション。
ヘッダー——最初のドットの前の部分。トークンに関するメタデータ、使用された署名アルゴリズム、トークンの種類にデコードされる。
ペイロード——中間のセクション。実際のクレーム:ユーザーID、メール、ロール、有効期限にデコードされる。バックエンドがユーザーを識別するために読む内容だ。
署名——2番目のドットの後の部分。トークンが改ざんされていないことの暗号学的証明。
ほとんどの開発者が驚くこと
ペイロードはBase64urlエンコードされている。暗号化されていない。トークンを持つ誰でもペイロードをデコードしてその内容を読める——ユーザーID、メール、ロール、有効期限、そこに入れたものすべて。
任意のJWTをJWTデコーダーに貼り付けて、中に正確に何があるかを見てみよう。鍵は不要だ。
これが人々を驚かせる理由は、文字列が不透明な意味不明のように見えるからだ。でも違う。JWTのセキュリティモデルは:「私はこのトークンが本当に私のサーバーから発行されたことを確認できる(署名検証)から、それが含むクレームを信頼できる。でもそれらのクレーム自体は秘密ではない」というものだ。
実用的な意味:JWTペイロードにパスワード、支払いデータ、機密個人情報を入れないこと。ユーザーを識別・認可するために必要な最小限のものだけを入れよう。
署名が本当に保証すること
署名は、ヘッダー+ペイロード+サーバーだけが知るシークレットキーをハッシュ関数に通すことで作成される。サーバーがトークンを受け取ると、同じシークレットを使って署名を再計算する。再計算された署名がトークンの署名と一致すれば、トークンは本物で改ざんされていない。
誰かがペイロードを変更してユーザーIDを123から456に変えると、署名検証が失敗する。
だからJWT署名シークレットは本当に秘密でなければならない。攻撃者がそれを手に入れたら、トークンを偽造して任意のユーザーになりすませる。
ハッシュ:一方向関数
ハッシュ関数は任意の入力を受け取り、固定サイズの出力を生成する。2つのプロパティがセキュリティに役立つ:
決定論的:同じ入力は常に同じ出力を生成する。
一方向性:ハッシュを逆算して元の入力を得ることはできない。unhash()は存在しない。
SHA-256("パスワード123") → ef92b778bafe771207...
SHA-256("パスワード124") → 88d4266fd4e6338d13...
1文字の違い。まったく異なるハッシュ。似た入力と似た出力の間に数学的な関係はない。
パスワードが暗号化ではなくハッシュされる理由
パスワードが暗号化されていれば、正しい鍵で復号できる。攻撃者がデータベースと暗号化キーを盗んだ場合、すべてのユーザーのパスワードが露出する。
ハッシュを使う場合:パスワードではなくハッシュを保存する。ユーザーがログインするとき、入力されたものをハッシュし、保存されたハッシュと比較する。元のものを取り出す必要は一切ない——試みを検証するだけでいい。
攻撃者がデータベースを手に入れても、ハッシュしか持っていない。パスワードを得るには推測して検証しなければならない、これははるかに遅い。
MD5、SHA-256、bcrypt——どれを使うかが重要な理由
MD5は2000年代初頭の標準だった。今はセキュリティ目的では破られている。現代のGPUは毎秒数十億のMD5ハッシュを計算でき、データベースを持つ攻撃者は数分で単純なパスワードを解読できる。パスワードに絶対MD5を使ってはいけない。
SHA-256は整合性確認に適切だ——ファイルが破損していないことを確認する、WebhookのHMAC署名を生成する。しかしパスワードには速すぎる。高速ハッシュはパスワードに悪い、なぜならブルートフォース攻撃を安くするからだ。
bcryptはパスワード専用に設計されている。意図的に遅い——実行する計算ラウンド数を調整できる——そして2つの同一パスワードが完全に異なるハッシュを生成することを保証するビルトインの「ソルト」(ランダムデータ)が含まれている。
ルール:パスワードにはbcrypt(またはArgon2またはscrypt)を使う。整合性確認が必要なそれ以外のものにはSHA-256を使う。
クイックリファレンス
| 概念 | 何か | 可逆? | 何のため |
|---|---|---|---|
| Base64 | バイナリ→テキストエンコーディング | はい、簡単に | テキストシステムを通じてバイナリデータを転送 |
| JWT | 署名されたJSONトークン | ペイロード:はい。署名:いいえ | ステートレス認証 |
| SHA-256 | ハッシュ関数 | いいえ | ファイル整合性、HMAC署名 |
| bcrypt | パスワードハッシュ | いいえ | ユーザーパスワードの保存 |
これら3つを正しく理解することは、コードレビューでは正しく見えて本番でのみ発見されるセキュリティエラーのクラスを防ぐ——その時にはすでに損害が発生している。
ツール:Base64、JWTデコーダー、ハッシュジェネレーター。結果を得るためだけでなく、理解をテストするために使おう。