手を振るまもるくんゆるふわサイバーセキュリティ
監視中のまもるくん

パスワードの安全な保存をPythonで実装する|ソルトとハッシュ反復

2026-09-08実践

#パスワード#ハッシュ#ソルト#Python#実践

サービスがパスワードを平文で保存するのは絶対NG——漏洩したら全ユーザーが即アウトです。ではどう保存するのか。答えは「ソルト付きハッシュを何万回も反復する」。この記事ではPythonの標準ライブラリだけで、正しいパスワード保存を実装します。パスワードの基礎の"実装版"です。

なぜ単純なハッシュではダメか

「ハッシュ化すれば安全」は半分正解、半分間違いです。素のハッシュには2つの弱点があります。

⚠️

素のハッシュの弱点

① 同じパスワードは同じハッシュになる → 使い回しがバレる/レインボーテーブルで逆引きされる。② ハッシュは高速 → 攻撃者が1秒に何十億回も総当たりできる。この2つを潰すのが「ソルト」と「反復」です。

安全な保存の3要素

🧂

ソルト

毎回ランダム付与

🔁

反復

わざと遅くする

💾

保存

ソルト+ハッシュ

🔑 ハッシュ体験(SHA-256)

文字を打つと即ハッシュ化。上下でたった1文字違うだけで、値がどれだけ変わるか見てみましょう

AとBのハッシュは 0% の文字が違います

青い文字が「異なる桁」。入力が1文字違うだけで、ほぼ全体が別物に変わります(雪崩効果)

ハッシュは一方通行:値から元の文字は戻せません。だからパスワード保管や改ざん検知に使われます

準備

標準ライブラリの hashliboshmac だけ。追加インストール不要です。

① パスワードをハッシュ化する

ユーザーごとにランダムなソルトを作り、PBKDF2で何万回も反復してハッシュします。

import hashlib, os

def hash_password(password, iterations=200_000):
    salt = os.urandom(16)                       # ユーザーごとにランダム
    dk = hashlib.pbkdf2_hmac(
        "sha256", password.encode(), salt, iterations
    )
    # ソルトと反復回数も一緒に保存する(あとで検証に必要)
    return f"{iterations}${salt.hex()}${dk.hex()}"

stored = hash_password("correct horse battery staple")
print(stored)   # 200000$<salt>$<hash> の形式で保存
🌱

ソルトは秘密じゃない

ソルトは一緒に保存してOKです。目的は「同じパスワードでもハッシュを変える」「レインボーテーブルを無効化する」こと。秘密にする必要はなく、ユーザーごとに違うことが大事です。

② ログイン時に検証する

保存した文字列からソルトと反復回数を取り出し、入力を同じ手順でハッシュして突き合わせます。

import hmac

def verify_password(password, stored):
    iterations, salt_hex, hash_hex = stored.split("$")
    salt = bytes.fromhex(salt_hex)
    dk = hashlib.pbkdf2_hmac(
        "sha256", password.encode(), salt, int(iterations)
    )
    # タイミング攻撃を防ぐため compare_digest で比較
    return hmac.compare_digest(dk.hex(), hash_hex)

print(verify_password("correct horse battery staple", stored))  # True
print(verify_password("wrong password", stored))                # False

正しいパスワードだけ True平文を保存せず、照合だけができる——これがパスワード保存の基本形です。

💡

== ではなく compare_digest

ハッシュの比較に普通の == を使うと、一致する文字数で処理時間が変わり、攻撃者に情報を与えます(タイミング攻撃)。hmac.compare_digest は時間を一定に保つので、こうした比較には必ずこちらを使います。

③ 反復回数は「わざと遅く」

反復回数を上げると検証は遅くなりますが、それが狙いです。攻撃者の総当たりも同じだけ遅くなります。

import time
for it in (10_000, 200_000, 1_000_000):
    t = time.time()
    hashlib.pbkdf2_hmac("sha256", b"pw", b"salt1234", it)
    print(f"{it:>9}回: {(time.time()-t)*1000:.1f} ms")

回数が増えるほど時間が伸びます。正規ユーザーは1回だけなので体感ゼロ、攻撃者は何十億回なので致命的。この非対称性が防御の核心です。実務ではPBKDF2に加え、bcrypt・scrypt・Argon2といった専用アルゴリズムも広く使われます。

まとめ

  • パスワードは平文保存NG。ソルト付きハッシュを反復して保存する
  • ソルトはユーザーごとにランダム。一緒に保存してよい(秘密ではない)
  • 反復(ストレッチング)でわざと遅くし、総当たりを非現実的にする
  • 比較は hmac.compare_digest でタイミング攻撃を防ぐ

もう少し詳しく(背景と理論)

パスワードは平文でも単純ハッシュでも保存してはいけません。攻撃者は事前計算したレインボーテーブルで高速に逆引きできるからです。対策の要はソルト(各ユーザー固有の乱数を混ぜる)とストレッチング(意図的に計算を重くして総当たりを遅くする)です1。標準的な選択肢は PBKDF2(RFC 8018)、bcrypt、scrypt、そして2015年の Password Hashing Competition で選ばれた Argon2 です2。とくに Argon2 や scrypt はメモリを大量に使う設計で、GPU/ASIC による大規模並列攻撃に強いのが特長です。OWASP はワークファクタ(反復回数・メモリ量)をハードウェアの進歩に合わせて定期的に引き上げるよう推奨しています3

次の一歩 🌸

土台の考え方はパスワードの基礎、次の防御層は多要素認証(MFA)、パスワード不要の未来はパスキーへどうぞ。

Footnotes

  1. ソルトはレインボーテーブルを無効化し、同じパスワードでも異なるハッシュにする。ストレッチングは1回の検証を意図的に遅くし、総当たり速度を下げる。

  2. Argon2 は Password Hashing Competition (2015) の勝者。メモリハード設計で GPU/ASIC 攻撃に強い。PBKDF2 / bcrypt / scrypt も広く使われる。

  3. OWASP Password Storage Cheat Sheet はアルゴリズム選択と最小ワークファクタの指針を示す。ハードウェア進化に応じてパラメータを更新する。

🔥 この分野の最新トレンドをチェック →

あわせて読みたい