安全な乱数生成器への予測可能なシード

推測可能な値だけで安全な乱数生成器を初期化しない

説明

システム時刻や固定値だけで安全な乱数生成器を初期化すると、攻撃者がシードを推測し、トークンの探索範囲を絞れる可能性があります。SecureRandomの動作はプロバイダーに依存し、同じシードから同じ列を出力するとは限りません。

Java APIの説明では、PRNG方式のSecureRandomで最初のnextBytes()より前にsetSeed()を呼ぶと自動シーディングが行われなくなる場合があり、呼び出し側が十分なエントロピーを用意する必要があります。一方、既存のシードに対するsetSeed()は置き換えではなく補充です。時刻を追加しただけで、すでに得たエントロピーが失われたとはいえません。

想定される影響

  • トークンやセッション識別子の推測によるアカウント・セッションの乗っ取り
  • 予測可能なCSRFトークンなどを使うリクエストの偽造
  • パスワード再設定やワンタイム認証コードの推測
  • 時刻に依存した初期化による総当たりの探索範囲の縮小

対処方法

  • 通常のトークン生成にはnew SecureRandom()を使い、プロバイダーのエントロピーによる初期化に任せてください。最初の使用前に時刻や定数だけでシーディングしないでください。
  • 明示的なシードが必要なら、十分に予測困難な値を使ってください。generateSeed(n)は別の生成器を初期化するバイト列を得るAPIであり、通常は個別のシーディングは不要です。
  • セキュリティトークンやセッション識別子にjava.util.Randomを使わないでください。十分な乱数長、有効期限、用途に応じた再利用制限を設けます。Base64は表現形式であり、エントロピーを増やしません。
  • 暗号鍵にはKeyGeneratorなどの専用APIを使ってください。パスワードから鍵を導出する場合は、適切なパスワードベースの鍵導出関数を使用します。

例

変更前

java
import java.security.SecureRandom;
import java.util.Base64;

public class InsecureTokenService {
    // 危険: 時刻で初期化
    public String issueToken() {
        SecureRandom rng = new SecureRandom();
        rng.setSeed(System.currentTimeMillis()); // 推測可能
        byte[] buf = new byte[16]; // 128ビット
        rng.nextBytes(buf);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
    }
}

変更後

java
import java.security.SecureRandom;
import java.util.Base64;

public class SecureTokenService {
    private static final SecureRandom RNG = new SecureRandom(); // プロバイダーのエントロピーで自動初期化

    public String issueToken() {
        byte[] buf = new byte[32]; // 256ビットの乱数
        RNG.nextBytes(buf);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
    }

    // 明示的シードが必要な場合のみ、ランダムなシードを取得
    public String issueTokenWithExplicitSeed() {
        SecureRandom sr = new SecureRandom();
        byte[] strongSeed = sr.generateSeed(32); // 予測困難なシード
        SecureRandom seeded = new SecureRandom(strongSeed);
        byte[] buf = new byte[32];
        seeded.nextBytes(buf);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(buf);
    }
}

変更前は最初の乱数生成より前に、推測しやすい時刻でシーディングします。変更後の通常経路はプロバイダーの自動初期化に任せます。追加のメソッドは別途ランダムなシードを得る例であり、通常のトークン生成に必須の手順ではありません。

参考資料