Sec-01: パスワードハッシング(PBKDF2)
javax.crypto と java.security を使って、 PBKDF2 による安全なパスワードハッシングを実装します。 MD5 / SHA-1 の危険性とタイミング攻撃対策も解説します。
なぜパスワードをそのまま保存してはいけないのか
ユーザーのパスワードをデータベースに平文(そのままの文字列)で保存することは非常に危険です。 データベースが漏えいした場合、すべてのパスワードが攻撃者に知られてしまいます。 パスワードは必ず「ハッシュ化」して保存する必要があります。
しかし、MD5 や SHA-1 などの一般的なハッシュ関数はパスワード保存に適していません。 これらは「高速に計算できる」ことを目的に設計されているため、 GPU を使った総当たり攻撃(ブルートフォース攻撃)で短時間に解析されてしまいます。
PBKDF2 の仕組み
- ストレッチング: ハッシュ計算を何万回も繰り返すことで、1回のハッシュ計算を意図的に遅くします。 OWASP は 2023 年時点で 310,000 回以上を推奨しています。
- ソルト(salt): パスワードごとにランダムなバイト列(ソルト)を付加します。 同じパスワードでも異なるハッシュになるため、レインボーテーブル攻撃(事前計算済みハッシュ一覧を使った攻撃)を防ぎます。
- タイミング攻撃対策: ハッシュ比較には
MessageDigest.isEqual()を使います。 通常の==やequals()は最初の不一致で処理を打ち切るため、 応答時間の差から情報が漏れる恐れがあります(タイミング攻撃)。
サンプルコード
Java 8 から PBKDF2WithHmacSHA256 が利用できます。パスワードは String ではなく char[] で扱い、使用後に clearPassword() でメモリからクリアするのが安全です。
よくあるミス・注意点
⚠️ MD5 / SHA-1 をパスワードハッシュに使う
MD5 や SHA-1 は高速に計算できるため、GPU を使うと1秒間に数十億回ものハッシュを試せます。 現代では短いパスワードなら数分で解析されます。パスワード保存には必ず PBKDF2・bcrypt・scrypt・Argon2 などの パスワードハッシュ専用アルゴリズムを使いましょう。
⚠️ ソルトを固定値にする
全ユーザーで同じソルトを使うと、同じパスワードは同じハッシュになります。 攻撃者は1つのレインボーテーブルですべてのユーザーのパスワードを解析できてしまいます。 ソルトは SecureRandom で ユーザーごとにランダム生成し、ハッシュと一緒に保存してください。
⚠️ == や equals() でハッシュ比較をする
通常の文字列比較は最初に不一致の文字を見つけた時点で処理を終了します。 このため、比較にかかる時間が入力によって変わり、応答時間の差から情報が漏れる 「タイミング攻撃」に脆弱になります。ハッシュ比較には必ずMessageDigest.isEqual() を使ってください。 これは常に一定時間で比較する「定数時間比較」を行います。
⚠️ パスワードを String で保持する
Java の String はイミュータブル(変更不可)なため、 使用後にメモリから消去できません。ガベージコレクションが実行されるまでメモリに残り続けます。 パスワードは char[] で扱い、 使用後に Arrays.fill(password, '\0') やspec.clearPassword() でメモリをクリアしましょう。
テストする観点
- 同じパスワード + 異なるソルトでハッシュを生成した場合、結果が異なること(ソルトの有効性)
- 正しいパスワードで
verifyPassword()を呼ぶとtrueが返ること - 誤ったパスワードで
verifyPassword()を呼ぶとfalseが返ること - 空文字列のパスワードでもハッシュ化・検証が正常に動作すること(境界値)
- 非常に長いパスワード(例: 1000 文字)でも正常に処理できること(境界値)
- 生成されるソルトが毎回異なること(SecureRandom の有効性)
- ソルトを変えると同じパスワードでも検証が
falseになること