第2部 セキュリティ編
第12章 暗号・ハッシュ・パスワード管理
「機密性を守る技術」の中核が暗号とハッシュです。混同されがちな両者の違いを整理し、パスワードの正しい保存方法を身につけます。
🎯 この章で学ぶこと
- ハッシュと暗号化の違い
- 共通鍵暗号と公開鍵暗号
- パスワードの正しい保存方法(ソルト付きハッシュ)
secretsによる安全な乱数・トークン生成- TLSと電子署名の考え方
暗号アルゴリズムを自作しないこと。暗号は世界中の専門家が何十年もかけて検証してきた領域です。素人の自作暗号はほぼ確実に破られます。私たちの仕事は「実績ある部品を、正しく使う」ことです。この章もその使い方を学ぶものです。
12.1 ハッシュと暗号化はまったく別物
この2つの違いを明確にすることが、この章で最も重要です。
| ハッシュ(hash) | 暗号化(encryption) | |
|---|---|---|
| 方向 | 一方向(元に戻せない) | 双方向(鍵で復号できる) |
| 目的 | 完全性の検証、パスワード保存 | 機密性の保持(中身を隠す) |
| 鍵 | 不要 | 必要 |
| 例 | SHA-256, bcrypt | AES, 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章では、この仕組みを使った「ファイル整合性チェッカー」を実際に作ります。MD5 や SHA-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章の注意点を守って)。
通常の == は「最初に違う文字が出た時点で」比較を打ち切るため、比較にかかる時間の差から正解を推測される恐れがあります(タイミング攻撃)。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。トークン、パスワード、ソルト、セッションID、認証コードはすべて secrets です。
12.5 共通鍵暗号と公開鍵暗号
暗号化(双方向)には大きく2方式があります。
- 共通鍵暗号(対称鍵) — 暗号化と復号に同じ鍵を使う。高速。代表はAES。課題は「鍵をどう安全に相手へ渡すか」
- 公開鍵暗号(非対称鍵) — 公開鍵で暗号化し、対応する秘密鍵でしか復号できない。鍵配送問題を解決する。代表はRSAや楕円曲線暗号。低速
実際の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(トークン)などは、すべてこの仕組みの上に成り立っています。
| 守りたいもの | 使う技術 |
|---|---|
| 機密性(中身を隠す) | 暗号化(AES / RSA / Fernet) |
| 完全性(改ざん検知) | ハッシュ / 電子署名 |
| 真正性(本人確認) | 電子署名 / 証明書 |
第9章のCIAが、ここで具体的な技術と結びつきます。
まとめ
- ハッシュは一方向(指紋)、暗号化は双方向(金庫)。目的が違う
- パスワードはソルト付き・低速なハッシュ(Argon2/bcrypt/PBKDF2)で保存する
- 推測されては困る値の生成は
secrets。randomは使わない - 照合には
secrets.compare_digest()(タイミング攻撃対策) - 暗号は自作しない。実績あるライブラリを正しく使い、鍵を安全に管理する
練習問題
次の用途には「ハッシュ」と「暗号化」のどちらが適切ですか。①保存したファイルが後で改ざんされていないか確認したい ②設定ファイルに書くDBパスワードを、読める形で暗号化して保管したい ③ログインパスワードをDBに保存したい
解答を見る
- ① ハッシュ(完全性の検証。元に戻す必要はない)
- ② 暗号化(後で復号して使うので双方向が必要)
- ③ ハッシュ(それもソルト付きの低速なパスワードハッシュ)。パスワードは復号する必要がないので、暗号化ではなくハッシュが正解です。復号できてしまう=漏れたら読める、を避けます。
secrets を使って、「英数字10桁の一時パスワード」を生成する関数 temp_password() を書いてください。ヒント:string.ascii_letters + string.digits と secrets.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 を使う点が重要です。一時パスワードは推測されてはならない値だからです。
「わが社は独自の暗号アルゴリズムを開発したので、他社より安全です」という主張の問題点を、この章の内容を踏まえて指摘してください。
解答を見る
暗号の安全性は公開された上で世界中の専門家に長年攻撃されても破られないことによって初めて担保されます(ケルクホフスの原理:アルゴリズムが公開されても鍵さえ秘密なら安全であるべき)。「独自で非公開だから安全」という考え方は隠蔽によるセキュリティ(security by obscurity)と呼ばれ、検証を受けていないぶん、むしろ危険です。自作暗号は避け、AESやRSAのような公的に標準化・検証されたアルゴリズムを使うべきです。