概要

パスワードの保存は、業務システムでもっとも慎重に扱うべき処理の一つです。平文保存はもちろん、MD5 や SHA-256 の単純ハッシュも GPU による総当たり攻撃に対して実質的な防御になりません。OWASP が推奨するのは、ソルト付きのストレッチングハッシュ(PBKDF2・bcrypt・scrypt・Argon2 など)です。Java では PBKDF2WithHmacSHA256 が標準 API だけで使えるため、外部ライブラリなしで安全なパスワードハッシュを実装できます。この記事では、ソルトの生成、ハッシュの計算、パスワード検証の一連の流れを実装し、タイミング攻撃を防ぐ定数時間比較や、パスワード文字列のメモリクリアといった実務上見落としやすいポイントも整理します。

使いどころ

社内システムのユーザー登録画面で、入力されたパスワードを PBKDF2 でハッシュ化してデータベースに保存する

ログイン認証時に入力パスワードのハッシュと保存済みハッシュを定数時間比較で照合する

既存システムの MD5 ハッシュを PBKDF2 へ移行するため、初回ログイン時に再ハッシュする仕組みを組み込む

コード例

PasswordHashingExample.java
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));
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

var と record を使ってハッシュとソルトを HashResult にまとめられる。コードの見通しが良くなり、返り値の受け渡しが安全になる。

Java 17
// Java 17: record でハッシュとソルトをまとめる
record HashResult(byte[] hash, byte[] salt) {}
var result = hashNewPassword(password);
// record から取り出して検証
boolean valid = verify(inputPassword, result);

Library Comparison

標準 API(PBKDF2WithHmacSHA256)外部依存なしでパスワードハッシュを実装したいとき。Java 8 以降のどの環境でも動作する。bcrypt や Argon2 に比べてメモリハード性がなく、ASIC 攻撃への耐性はやや劣る。ただし適切なイテレーション回数を設定すれば実務上は十分。
jBCryptbcrypt アルゴリズムを使いたいとき。コスト係数の調整が直感的で、Rails 等の他言語システムとの互換性がある。外部依存が増える。Java 標準 API だけで済む要件なら PBKDF2 で十分な場合が多い。
Argon2-jvmメモリハード関数で最高水準のセキュリティが求められるとき。Password Hashing Competition の勝者。ネイティブライブラリへの依存があり、環境構築のハードルが上がる。社内システムでは PBKDF2 や bcrypt で十分なケースが大半。

注意点

MD5 や SHA-256 の単純ハッシュをパスワード保存に使ってはいけない。高速すぎるため、GPU で秒間数十億回の試行が可能になる

ソルトは必ずユーザーごとにランダム生成すること。固定ソルトやソルトなしでは、レインボーテーブル攻撃に対して無防備になる

PBEKeySpec のインスタンスは使用後に clearPassword() を呼んでメモリ上のパスワードをクリアする。GC 任せにすると、ヒープダンプからパスワードが読み取られるリスクがある

パスワードの一致判定には MessageDigest.isEqual() を使う。Arrays.equals() は短絡評価のため、ハッシュの先頭バイトが一致するかどうかで処理時間が変わり、タイミング攻撃の手がかりになる

OWASP 推奨のイテレーション回数(2023年時点で310,000回)はハードウェアの進歩に合わせて見直す必要がある。定数としてコードに埋め込む場合でも、変更しやすい設計にしておくこと

FAQ

PBKDF2 のイテレーション回数はどのくらいに設定すべきですか。

OWASP は2023年時点で PBKDF2WithHmacSHA256 に対して310,000回を推奨しています。ログイン時の遅延が許容範囲(通常100ms〜500ms)に収まるかを実環境で計測して決めてください。

ソルトの長さはどのくらい必要ですか。

16バイト(128ビット)以上が推奨です。NIST SP 800-132 では128ビット以上のランダムソルトを求めています。SecureRandom で生成すれば十分なエントロピーが得られます。

String ではなく char[] でパスワードを扱う理由は何ですか。

String は不変でGCまでヒープに残りますが、char[] は使用後に Arrays.fill で上書きできます。ヒープダンプや core dump からパスワードが漏洩するリスクを軽減するためです。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます