概要
パスワードの保存は、業務システムでもっとも慎重に扱うべき処理の一つです。平文保存はもちろん、MD5 や SHA-256 の単純ハッシュも GPU による総当たり攻撃に対して実質的な防御になりません。OWASP が推奨するのは、ソルト付きのストレッチングハッシュ(PBKDF2・bcrypt・scrypt・Argon2 など)です。Java では PBKDF2WithHmacSHA256 が標準 API だけで使えるため、外部ライブラリなしで安全なパスワードハッシュを実装できます。この記事では、ソルトの生成、ハッシュの計算、パスワード検証の一連の流れを実装し、タイミング攻撃を防ぐ定数時間比較や、パスワード文字列のメモリクリアといった実務上見落としやすいポイントも整理します。
使いどころ
社内システムのユーザー登録画面で、入力されたパスワードを PBKDF2 でハッシュ化してデータベースに保存する
ログイン認証時に入力パスワードのハッシュと保存済みハッシュを定数時間比較で照合する
既存システムの MD5 ハッシュを PBKDF2 へ移行するため、初回ログイン時に再ハッシュする仕組みを組み込む
コード例
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;
public class PasswordHashingExample {
private static final int ITERATIONS = 310_000;
private static final int KEY_LENGTH = 256;
record HashResult(byte[] hash, byte[] salt) {}
/** ソルト生成(16バイト) */
public static byte[] generateSalt() {
var salt = new byte[16];
new SecureRandom().nextBytes(salt);
return salt;
}
/** PBKDF2 でハッシュを生成 */
public static byte[] hashPassword(char[] password, byte[] salt)
throws NoSuchAlgorithmException, InvalidKeySpecException {
var spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);
try {
var factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return factory.generateSecret(spec).getEncoded();
} finally {
spec.clearPassword();
}
}
/** ソルト生成 + ハッシュを一括実行 */
public static HashResult hashNewPassword(char[] password)
throws NoSuchAlgorithmException, InvalidKeySpecException {
var salt = generateSalt();
var hash = hashPassword(password, salt);
return new HashResult(hash, salt);
}
/** パスワード検証(定数時間比較) */
public static boolean verify(char[] password, HashResult stored)
throws NoSuchAlgorithmException, InvalidKeySpecException {
var inputHash = hashPassword(password, stored.salt());
return MessageDigest.isEqual(inputHash, stored.hash());
}
public static void main(String[] args) throws Exception {
var password = "MySecureP@ssw0rd".toCharArray();
// ハッシュ生成
var result = hashNewPassword(password);
System.out.println("ソルト: " +
Base64.getEncoder().encodeToString(result.salt()));
System.out.println("ハッシュ: " +
Base64.getEncoder().encodeToString(result.hash()));
// 検証
System.out.println("正しい: " + verify(password, result));
System.out.println("誤り: " +
verify("wrong".toCharArray(), result));
}
}Version Coverage
var と record を使ってハッシュとソルトを HashResult にまとめられる。コードの見通しが良くなり、返り値の受け渡しが安全になる。
// Java 17: record でハッシュとソルトをまとめる
record HashResult(byte[] hash, byte[] salt) {}
var result = hashNewPassword(password);
// record から取り出して検証
boolean valid = verify(inputPassword, result);Library Comparison
注意点
MD5 や SHA-256 の単純ハッシュをパスワード保存に使ってはいけない。高速すぎるため、GPU で秒間数十億回の試行が可能になる
ソルトは必ずユーザーごとにランダム生成すること。固定ソルトやソルトなしでは、レインボーテーブル攻撃に対して無防備になる
PBEKeySpec のインスタンスは使用後に clearPassword() を呼んでメモリ上のパスワードをクリアする。GC 任せにすると、ヒープダンプからパスワードが読み取られるリスクがある
パスワードの一致判定には MessageDigest.isEqual() を使う。Arrays.equals() は短絡評価のため、ハッシュの先頭バイトが一致するかどうかで処理時間が変わり、タイミング攻撃の手がかりになる
OWASP 推奨のイテレーション回数(2023年時点で310,000回)はハードウェアの進歩に合わせて見直す必要がある。定数としてコードに埋め込む場合でも、変更しやすい設計にしておくこと
FAQ
OWASP は2023年時点で PBKDF2WithHmacSHA256 に対して310,000回を推奨しています。ログイン時の遅延が許容範囲(通常100ms〜500ms)に収まるかを実環境で計測して決めてください。
16バイト(128ビット)以上が推奨です。NIST SP 800-132 では128ビット以上のランダムソルトを求めています。SecureRandom で生成すれば十分なエントロピーが得られます。
String は不変でGCまでヒープに残りますが、char[] は使用後に Arrays.fill で上書きできます。ヒープダンプや core dump からパスワードが漏洩するリスクを軽減するためです。