🎯 この章で学ぶこと

  • ハッシュと暗号化の違い
  • 共通鍵暗号と公開鍵暗号
  • パスワードの正しい保存方法(ソルト付きハッシュ)
  • secrets による安全な乱数・トークン生成
  • TLSと電子署名の考え方
⚠️ 最重要の大原則

暗号アルゴリズムを自作しないこと。暗号は世界中の専門家が何十年もかけて検証してきた領域です。素人の自作暗号はほぼ確実に破られます。私たちの仕事は「実績ある部品を、正しく使う」ことです。この章もその使い方を学ぶものです。

12.1 ハッシュと暗号化はまったく別物

この2つの違いを明確にすることが、この章で最も重要です。

ハッシュ(hash)暗号化(encryption)
方向一方向(元に戻せない)双方向(鍵で復号できる)
目的完全性の検証、パスワード保存機密性の保持(中身を隠す)
不要必要
SHA-256, bcryptAES, RSA

覚え方:ハッシュは「指紋」、暗号化は「金庫」。指紋から本人を復元することはできませんが、同じ人なら常に同じ指紋になります。金庫は鍵があれば開け閉めできます。

12.2 ハッシュを計算する — hashlib

ハッシュは、任意のデータから固定長の要約値を作る一方向の計算です。標準ライブラリ hashlib で計算できます。

import hashlib

data = "Hello, Security!"
digest = hashlib.sha256(data.encode("utf-8")).hexdigest()
print(digest)      # 64文字の16進文字列(常に同じ入力→同じ出力)

# 1文字変えるとハッシュは全く変わる(雪崩効果)
print(hashlib.sha256("Hello, Security?".encode()).hexdigest())

ハッシュの性質は3つです。

🛡️ セキュリティの視点 — ハッシュはファイルの改ざん検知に使える

配布ファイルのハッシュ値を公開しておけば、ダウンロードした側が同じハッシュを計算して照合することで改ざんや破損を検知できます(完全性の検証)。第13章では、この仕組みを使った「ファイル整合性チェッカー」を実際に作ります。MD5SHA-1 は衝突が見つかっており非推奨です。SHA-256以上を使いましょう。

12.3 パスワードを「正しく」保存する

ここはセキュリティで最も事故が多い領域です。段階を追って「なぜダメか」を理解しましょう。

❌ 論外:平文で保存

DBが漏えいした瞬間、全員のパスワードが即座に露出します。使い回しにより他サービスの被害にも波及します。絶対に不可

❌ 不十分:ただのSHA-256でハッシュ化

一見安全そうですが、2つの弱点があります。①同じパスワードは同じハッシュになるため、漏えいDB内で同一パスワードの利用者が一目でわかる。②高速に計算できるため、攻撃者は事前計算表(レインボーテーブル)や総当たりで元パスワードを高速に逆引きできます。

✅ 正解:ソルト付きの「低速な」パスワードハッシュ

対策は2つです。ソルト(利用者ごとに異なるランダムな文字列)を加えてから計算することで、①同じパスワードでもハッシュが変わり、②事前計算表が無効になります。加えて、パスワード専用の意図的に低速なアルゴリズム(bcrypt、scrypt、Argon2、PBKDF2)を使い、総当たりのコストを上げます。

まず標準ライブラリだけでできるPBKDF2の例です。

import hashlib
import secrets

def hash_password(password):
    """ソルト付きPBKDF2でパスワードをハッシュ化する"""
    salt = secrets.token_bytes(16)                 # 利用者ごとのランダムなソルト
    dk = hashlib.pbkdf2_hmac(
        "sha256", password.encode("utf-8"), salt,
        200_000,                                   # 反復回数(多いほど低速=安全)
    )
    # 検証に必要なソルトと反復回数も一緒に保存する
    return salt.hex() + "$" + dk.hex()

def verify_password(password, stored):
    """入力パスワードが保存済みハッシュと一致するか検証する"""
    salt_hex, dk_hex = stored.split("$")
    salt = bytes.fromhex(salt_hex)
    dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 200_000)
    # タイミング攻撃を防ぐため、通常の == ではなく compare_digest を使う
    return secrets.compare_digest(dk.hex(), dk_hex)

stored = hash_password("CorrectHorse2026")
print(verify_password("CorrectHorse2026", stored))   # True
print(verify_password("wrong", stored))              # False
✅ 実務ではライブラリに任せる

上のコードは仕組みを理解するための教材です。実務では、より新しく推奨される Argon2(argon2-cffi)や bcrypt ライブラリを使うのが一般的です。これらはソルトの生成・保存・検証を安全に一括で行ってくれます。pip install argon2-cffi のように導入します(第7章の注意点を守って)。

💡 compare_digest とタイミング攻撃

通常の == は「最初に違う文字が出た時点で」比較を打ち切るため、比較にかかる時間の差から正解を推測される恐れがあります(タイミング攻撃)。secrets.compare_digest() は入力長にかかわらず一定時間で比較するため、秘密情報の照合にはこちらを使います。

12.4 安全な乱数とトークン — secrets

パスワード再設定リンク、セッションID、APIトークンなど「推測されては困る値」の生成には、必ず secrets モジュールを使います。

import secrets
import random

# ✅ 暗号学的に安全(こちらを使う)
print(secrets.token_urlsafe(32))    # URLで使える安全なトークン
print(secrets.token_hex(16))        # 16進のトークン
print(secrets.randbelow(1000000))   # 0〜999999 の安全な乱数(ワンタイムコード等)

# ❌ random は予測可能。セキュリティ用途に使ってはいけない
print(random.randint(0, 999999))    # ゲームやシミュレーション専用
⚠️ random と secrets の使い分け

random モジュールは高速ですが予測可能で、セキュリティ用途には使えません。「他人に当てられたら困る値」は例外なく secrets。トークン、パスワード、ソルト、セッションID、認証コードはすべて secrets です。

12.5 共通鍵暗号と公開鍵暗号

暗号化(双方向)には大きく2方式があります。

実際のTLS(HTTPS)は両者の「いいとこ取り」をします。最初に公開鍵暗号で共通鍵を安全に共有し、以降の大量の通信は高速な共通鍵暗号で行うハイブリッド方式です。

Pythonで対称鍵暗号を扱うなら、cryptography ライブラリの Fernet が安全で扱いやすい定番です。

# pip install cryptography が必要
from cryptography.fernet import Fernet

key = Fernet.generate_key()      # 鍵を安全に生成(この鍵の保管が最重要)
f = Fernet(key)

token = f.encrypt(b"secret data")   # 暗号化
print(token)
print(f.decrypt(token))             # b'secret data' に復号

Fernet は認証付き暗号で、鍵の管理さえ正しく行えば安全に使えます。鍵はコードに書かず(第10章)、環境変数やシークレットマネージャで管理します。

12.6 電子署名 — 「本物であること」を証明する

公開鍵暗号は逆向きにも使えます。秘密鍵で署名し、公開鍵で検証すると、「確かに秘密鍵の持ち主が作った/送った」ことと「改ざんされていない」ことを証明できます。これが電子署名で、以下を同時に実現します。

HTTPSのサーバー証明書、ソフトウェアの署名、JWT(トークン)などは、すべてこの仕組みの上に成り立っています。

🛡️ セキュリティの視点 — 暗号の3つの用途を整理
守りたいもの使う技術
機密性(中身を隠す)暗号化(AES / RSA / Fernet)
完全性(改ざん検知)ハッシュ / 電子署名
真正性(本人確認)電子署名 / 証明書

第9章のCIAが、ここで具体的な技術と結びつきます。

まとめ

練習問題

問題 12-1

次の用途には「ハッシュ」と「暗号化」のどちらが適切ですか。①保存したファイルが後で改ざんされていないか確認したい ②設定ファイルに書くDBパスワードを、読める形で暗号化して保管したい ③ログインパスワードをDBに保存したい

解答を見る
  • ハッシュ(完全性の検証。元に戻す必要はない)
  • 暗号化(後で復号して使うので双方向が必要)
  • ハッシュ(それもソルト付きの低速なパスワードハッシュ)。パスワードは復号する必要がないので、暗号化ではなくハッシュが正解です。復号できてしまう=漏れたら読める、を避けます。
問題 12-2

secrets を使って、「英数字10桁の一時パスワード」を生成する関数 temp_password() を書いてください。ヒント:string.ascii_letters + string.digitssecrets.choice()

解答を見る
import secrets
import string

def temp_password(length=10):
    """英数字からなる一時パスワードを安全に生成する"""
    alphabet = string.ascii_letters + string.digits
    return "".join(secrets.choice(alphabet) for _ in range(length))

print(temp_password())   # 例: a8Kd2pXq7M

random.choice ではなく secrets.choice を使う点が重要です。一時パスワードは推測されてはならない値だからです。

問題 12-3

「わが社は独自の暗号アルゴリズムを開発したので、他社より安全です」という主張の問題点を、この章の内容を踏まえて指摘してください。

解答を見る

暗号の安全性は公開された上で世界中の専門家に長年攻撃されても破られないことによって初めて担保されます(ケルクホフスの原理:アルゴリズムが公開されても鍵さえ秘密なら安全であるべき)。「独自で非公開だから安全」という考え方は隠蔽によるセキュリティ(security by obscurity)と呼ばれ、検証を受けていないぶん、むしろ危険です。自作暗号は避け、AESやRSAのような公的に標準化・検証されたアルゴリズムを使うべきです。