説明
システム時刻や固定値だけで安全な乱数生成器を初期化すると、攻撃者がシードを推測し、トークンの探索範囲を絞れる可能性があります。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);
}
}
変更前は最初の乱数生成より前に、推測しやすい時刻でシーディングします。変更後の通常経路はプロバイダーの自動初期化に任せます。追加のメソッドは別途ランダムなシードを得る例であり、通常のトークン生成に必須の手順ではありません。