🐔

SecureRandom クラスと java.security.egd

2021/09/22に公開

背景

最近 java.security.SecureRandom クラスについていろいろ確認したので自分用の備忘録を兼ねて。
なお調べたのは Java 17 で Linux の場合。他のバージョンや OS だと異なったりするので注意。
一応 JDK のソースを見ながら確認したつもりだが、もし誤りがあればツッコミ頂けるとありがたい。

ちなみに、きっかけは SecureRandom のデフォルトコンストラクタで生成されたインスタンスが java.util.UUID.randomUUID() で使われていたから。
Tomcat のセッション ID 生成にも使われてるが、こちらはアルゴリズム指定のコンストラクタだった。(こちらのアルゴリズムのデフォルトは SHA1PRNG

ざっくりした結論

普通の環境で SecureRandom をデフォルトコンストラクタで生成すると、以下のようになる。

  1. システムプロパティ java.security.egd が設定されていない場合、および、java.security.egdfile:/dev/random に設定されている場合
    • アルゴリズムは NativePRNG になる。
    • generateSeed() メソッドは /dev/random から読みだす。
    • nextBytes() メソッドは /dev/urandom から読みだす。(が、読んだ結果がそのまま返るわけではない)
  2. java.security.egdfile:/dev/urandom に設定されている場合
    • アルゴリズムは NativePRNG になる。
    • generateSeed() メソッドは /dev/urandom から読みだす。
    • nextBytes() メソッドは /dev/urandom から読みだす。(が、読んだ結果がそのまま返るわけではない)
  3. java.security.egd が上記以外の URL に設定されていて、ちゃんと読める場合
    • アルゴリズムは DRBG(パラメータは Hash_DRBG,SHA-256,128,none) になる。
    • generateSeed() メソッドは指定されたデバイスから読みだす。
    • nextBytes() メソッドはパラメータに従って生成する。
  4. java.security.egd が上記以外の URL に設定されていて、読めない場合
    • アルゴリズムは DRBG(パラメータは Hash_DRBG,SHA-256,128,none) になる。
    • generateSeed() メソッドはダミースレッド等を生成して無理やり作り出す。
    • nextBytes() メソッドはパラメータに従って生成する。

1 や 2 にある java.security.egd のチェックは単純な文字列比較である。つまり、ネットで良く見かけるシステムプロパティの設定 java.security.egd=file:/dev/./urandom をした場合は、3 になる。

また、1 や 2 の nextBytes()java.security.egd で設定したデバイスから読んでいるわけではなく常に /dev/urandom から読んでいる上に、読んだ結果をそのまま返しているわけではない。知らなかった…

なお、どのケースでも nextBytes() のために乱数シードが必要になるのだが、そのシードに自分の generateSeed() が使われているわけではない。なぜなんだぜ…

そもそも java.security.egd って何だよ!

egd って何やねん。意味不明過ぎる。
と思ったら、セキュリティ設定用のプロパティファイル $JAVA_HOME/conf/security/java.security を見たら書いてあった。
Entropy Gathering Device
乱雑さを(entropy)集める(gathering)デバイス(device)って事ね。まぁ許してやるか。

ちなみに、$JAVA_HOME/conf/security/java.security に設定されているプロパティはセキュリティプロパティと呼ぶっぽい(JDK のソースにはそう書かれていた)ので、ここでもそのように呼ぶことにする。

以下、いろいろ調べたメモ

上記を調べる過程で他にもいろいろ調べたので、忘れないように書き留めておく。
単なるメモ書きなので全く纏まっていないが。

そもそも SecureRandom とは?

SecureRandom とはセキュアな乱数生成器である(まんま)。セキュリティ目的での使用に耐えられるように作られているらしい。(セキュリティ全く分からないマンなのでこれ以上は深入りしない)

デフォルトコンストラクタと byte[] をシードとして引数に取るコンストラクタがある。
また、コンストラクタとは別に、ファクトリメソッドも多数用意されている。getInstance() は引数のバリエーションが山ほどあるし(6 種類)、その他に引数の無い getInstanceStrong() って言う強そうな(?)のもある。

が、普通はデフォルトコンストラクタで生成するんじゃなかろうか(根拠なし)。
と言うわけで(?)、デフォルトコンストラクタで生成される乱数生成器を確認する。

ところで、getInstance() の引数を見ると何となく分かるが、この SecureRandom はサービスプロバイダインタフェースに従って作られている。つまり、実際の乱数を生成するのは何らかのプロバイダに委譲しているのである。そして、各プロバイダはそれぞれデフォルトの乱数生成器を持っている(事もあるし、持ってない事もある)。

SecureRandom のサービスプロバイダインタフェース

SecureRandom が使用しているサービスプロバイダインタフェースは SecureRandomSpi である(まんま)。
つまり、SecureRandom のインスタンスの裏では SecureRandomSpi を実装した何らかのクラスのインスタンスがせっせと働いているのである。

そして、メソッド名を見ればすぐに分かるように、以下のような対応関係になっている。(全部書くと鬱陶しいので以下で触れるものだけ抜粋)

SecureRandom のメソッド SecureRandomSpi のメソッド
nextBytes() engineNextBytes()
generateSeed() engineGenerateSeed()

SPI 側のメソッドはみんな engine と付いてるので乱数生成エンジンと言った感じか?知らんけど。

SecureRandom の乱数生成アルゴリズム

デフォルトコンストラクタで生成されるのは何者なのか。
SecureRandom からソースを辿っていくと、セキュリティプロパティ security.provider.<n><n>1 から連番)を頭から順に取ってきて使えるプロバイダを決定している。デフォルト(JDK をインストールした状態)の先頭は security.provider.1=SUN である。Sun、お前死んだはずでは!?

で、プロバイダが SUN の場合、デフォルトの乱数生成器は sun.security.provider.SunEntries クラスの DEF_SECURE_RANDOM_ALGO フィールドでアルゴリズムが指定されている。
では DEF_SECURE_RANDOM_ALGO は何になっているかと言うと、

  1. NativePRNG が使用可能で、かつ、sun.security.provider.SunEntries クラスの seedSource フィールドが文字列として file:/dev/urandomfile:/dev/random のいずれかと等しい場合
    NativePRNG
  2. 上記以外の場合
    DRBG

そう、最初に書いたようなデフォルトコンストラクタでのアルゴリズムの決定方法はココから来ている。

ちなみに、何らかの理由により上記の 2 つも含めて、設定されているセキュリティプロバイダのいずれの乱数生成器も使えない場合には SUN プロバイダの SHA1PRNG が使用される。(まぁ普通そんな事にはならないと思うが)

SunEntries.seedSource フィールドの設定内容

SunEntries.seedSource がどのように設定されているかを正確に書くと、以下のようになる。

  1. システムプロパティ java.security.egd が設定されている場合
    java.security.egd の設定値
  2. それが設定されていなくて、かつ、セキュリティプロパティ securerandom.source が設定されている場合
    securerandom.source の設定値
  3. それも設定されていない場合
    ⇒ 空文字列

ちなみに、セキュリティプロパティ securerandom.source はデフォルト(JDK をインストールした状態)で file:/dev/random になっている。
したがって、普通はシステムプロパティ java.security.egd を設定しなければ file:/dev/random になる。

/dev/random/dev/urandom

/dev/random/dev/urandom はいずれも乱数を生成し続けるデバイスだが、乱数生成の元となる「エントロピープール」が枯渇したときに /dev/random はエントロピープールにある程度データが溜まるまでブロックしてしまうが /dev/urandom はブロックせずに乱数を生成し続ける。らしい。

てことは /dev/random ではなく /dev/urandom を使用するとセキュリティ的に弱くなる気がするのだが、ホントにそうなのか、そうだとしたらどの程度違うものなのかは良く分からん。

NativePRNG とは?

NativePRNG と言う事だろう。

Native と言うのは、多分 OS のデバイスから取得したエントロピーソースをメインで使用していると言う事を表しているのだと思う(勘でモノを言っています)。

PRNGPseudo Random Number Generator
疑似(Pseudo)乱数(Random Number)生成器(Generator)。
ホンモノの乱数じゃないから Pseudo と言う事なんだと思うが、NativePRNG もニセモノって事なのだろうか。やっぱり良く分からんな。

NativePRNG の実装クラスは sun.security.provider.NativePRNG である。

NativePRNG が使用可能とはどういうことか

NativePRNG が使用可能とはどう言う事かと言うと、以下の 2 つの条件を満たしていると言う事である。

  1. シード用デバイスが読み出し可能である。
    ここで「シード用デバイス」は基本的に上記の SunEntries.seedSource フィールドの設定値である。
    しかし、当該フィールドが file プロトコルじゃない場合、あるいは、file プロトコルであっても読み出しができない場合は /dev/random にフォールバックする。
    つまり、最悪でも /dev/random が読み出し可能であれば OK という事である。
  2. 乱数用デバイスが読み出し可能である。
    ここで「乱数用デバイス」は /dev/urandom 固定である事に注意が必要である。プロパティ等で変更することは出来ない。

これらからすると、普通の Linux 環境で NativePRNG が使用可能ではないと言う事はそうそう起きないのではないかと思う。

NativePRNG.engineNextBytes() の挙動

最初に書いたように、NativePRNG の場合でも engineNextBytes() は乱数用デバイス(つまり /dev/urandom)から読んだ値そのものを返しているわけではない。
じゃあ何を返しているかと言うと、以下の 2 つからそれぞれ要求されたバイト数分だけ取得して、それらの結果を XOR して返している。

  1. /dev/urandom から読みだしたデータ
  2. インスタンス内部に持っている乱数生成器で生成した乱数

ややこしいが NativePRNG のインスタンス内には隠れたもう一つの乱数生成器があるのだ。

この隠れたもう一つの乱数生成器は SHA1PRNG アルゴリズムの SecureRandomSpi 実装クラス(ややこしいことに、sun.security.provider.SecureRandom である)であり、当該インスタンスの生成時には前述の「乱数用デバイス」から 20 バイト読みだしたものをシードにしている(「シード用デバイス」ではないことに注意)。最初に「どのケースでも nextBytes() のために乱数シードが必要になる」と書いたが、NativePRNG の場合に必要な「乱数シード」とはこの 20 バイトの事だ。

また、この隠れたもう一つの乱数生成器から取得する「乱数」は engineNextBytes() から取得するもので、engineGenerateSeed() からではない。(必要なのは「乱数」であって「シード」ではない、という事か)

なお、いずれにせよ読みだしているデバイスは「乱数用デバイス」、つまり /dev/urandom 固定であり、システムプロパティ java.security.egd の設定とは全く関係が無い。

つまり、NativePRNG を使用する限り、java.security.egd に何を設定しようが nextBytes() の挙動は変わらない、という事になる。(file:/dev/randomfile:/dev/urandom 以外を設定するとデフォルトコンストラクタでは NativePRNG は使われなくなるが)

ちなみに、NativePRNGengineGenerateSeed() の方は上で触れた「シード用デバイス」から読みだしたものそのままを返している。

NativePRNGBlockingNativePRNGNonBlocking

デフォルトコンストラクタでは生成され得ないが、NativePRNG の仲間に NativePRNGBlockingNativePRNGNonBlocking と言うアルゴリズムもある。
これらの実装クラスは NativePRNG のネストしたクラスとなっていて、それぞれ sun.security.provider.NativePRNG.Blockingsun.security.provider.NativePRNG.NonBlocking である。

これらは NativePRNG と挙動はほぼ一緒だが「シード用デバイス」と「乱数用デバイス」だけが異なる。

  1. NativePRNGBlocking
    シード用デバイス:/dev/random
    乱数用デバイス:/dev/random
    確かにどちらもブロックするデバイスだ。
  2. NativePRNGNonBlocking
    シード用デバイス:/dev/urandom
    乱数用デバイス:/dev/urandom
    確かにどちらもブロックしないデバイスだ。

ちなみに、システムプロパティ java.security.egdfile:/dev/urandom に設定すると、デフォルトコンストラクタで使用されるアルゴリズムは NativePRNG になり、かつ NativePRNG のシード用デバイスを /dev/urandom にする事が出来るので、明示的に NativePRNGNonBlocking を使用するのと同じ効果がある。

DRBG とは?

Deterministic Random Bit Generator
決定論的(Deterministic)ランダムビット(Random Bit)生成器(Generator)。
何でも「NIST Special Publication 800-90A Revision 1」というありがたいモノに基づいているらしい。
「決定論的」と言うのはアルゴリズムと各種パラメータが決まると常に同じ乱数列が生成されるから、と言う事で良いのだろうか。
しかしなんでこっちは Random Number じゃなくて Random Bit なんだ…

DRBG は単一のアルゴリズムではなく、パラメータを設定する事によっていろいろ挙動が変わるようである。

DRBG のパラメータ

DRBG のパラメータはセキュリティプロパティ securerandom.drbg.config で設定できる。このプロパティは以下のモノをカンマ区切りで結合したものである。記載は順不同で OK だ。(内容は全く分かっていない)

  1. メカニズム
    Hash_DRBGHMAC_DRBGCTR_DRBG のいずれか。
    Hash はそのままハッシュ、HMAC は Hash-based Message Authentication Code、CTR はカウンタ、と言う事でよろしいか?
    デフォルトは Hash_DRBG
  2. アルゴリズム
    • メカニズムが Hash_DRBGHMAC_DRBG の場合、以下のいずれかを使用可能。
      SHA-224SHA-512/224SHA-256SHA-512/256SHA-384SHA-512
      よく見かけるやつだな(知ってるとは言ってない)。
      デフォルトは SHA-256
    • メカニズムが CTR_DRBG の場合、以下のいずれかを使用可能。
      AES-128AES-192AES-256
      これもよく見かけるやつだな(知ってるとは言ってない)。
      デフォルトは AES-256
  3. セキュリティ強度
    112128192256 のいずれか。
    この数字は bit か?
    デフォルトは 128
  4. ケーパビリティ
    nonereseed_onlypr_and_reseed のいずれか。
    reseed は途中でシードを設定できる、と言う感じか。prprediction resistance らしい。
    デフォルトは none
  5. 導出関数(derivation function)
    メカニズムが CTR_DRBG の時のみ意味がある。
    use_dfno_df のいずれか。
    まんま導出関数を使うか否か、と言う感じだろうか。
    デフォルトは use_df

securerandom.drbg.config のデフォルト(JDK をインストールした状態)は設定無しである。つまり、全て上のデフォルトの状態である。したがって、Hash_DRBG,SHA-256,128,none と指定されている状態と同等である。

ちなみに、上記パラメータの一部は SecureRandomParameters を引数に取る SecureRandom.getInstance() メソッドでインスタンスを生成することで実行時に指定できる。
なお、SecureRandomParameters は単なるマーカーインタフェースで、実際に引数として渡すオブジェクトは、DrbgParameters.instantiation() メソッドで生成する。

DRBG のパラメータ詳細はここでは立ち入らない(説明できるほどわかっていないので立ち入れない)が、システムプロパティ java.security.egd とちょっとだけ関連するためケーパビリティだけ後述する。

DRBG の実装クラス

DRBG の実装クラスは sun.security.provider.DRBG である。
が、実際の処理は「メカニズム」毎に更に別の実装クラスに委譲されている。

ちょっと mermaid.js 使ってみたけど、何か文字ちいせぇな。まぁいいか。

で、見て分かるように sun.security.provider.AbstractDrbg が抽象クラスである。そしてこいつは SecureRandomSpi を実装しているわけではないが、同じように engineNextBytes()engineGenerateSeed() がある。ちなみにこれらのメソッドは final で、engineNextBytes() の方はテンプレートメソッド的な感じになっている。

どのクラスがどのメカニズムに対応してるかは名前を見れば分かると思う。

DRBG のケーパビリティと engineNextBytes() の挙動

DRBG のケーパビリティが pr_and_reseed か否かによって乱数生成器を初期化する際に使用されるシード(エントロピー入力)の生成方法が変わる。
engineGenerateSeed() のシード生成方法が変わるわけではない事に注意。

  1. ケーパビリティが pr_and_reseed の場合
    sun.security.provider.SeedGenerator クラスの generateSeed() を使用する。
  2. ケーパビリティが pr_and_reseed 以外の場合
    内部にある乱数生成器の engineNextBytes() を使用する。

1 のケースにある SeedGenerator.generateSeed() については後述する。

2 のケースではまた内部の乱数生成器を使っている。このパターン多いな。セキュリティ的によく使われる設計なんだろうか。

で、この内部の乱数生成器であるが、AbstractDrbg クラスの static フィールドに持っている HashDRBG のインスタンスである。
この内部の乱数生成器のパラメータは Hash_DRBG,SHA-256,256,none(デフォルトと暗号強度だけ違う)であるが、この乱数生成器の初期化に使用されるシードの生成方法はちょっとだけ特殊で、ケーパビリティが none であるにもかかわらず pr_and_reseed の場合のように SeedGenerator.generateSeed() を使用する。
まぁそうしないと engineNextBytes() を使用するのに自分の engineNextBytes() の結果が必要になってしまって永久に乱数の生成が出来なくなるので、それを回避するためにそうなってるのだろう。知らんけど。

また、内部の乱数生成器の生成時には、SeedGenerator.getSystemEntropy() で取得できるエントロピー入力も併せて使用される。

ちなみに、上記で static フィールドに持っていると書いたが、実はより正確に言うと、static フィールドに設定されているラムダ式がキャプチャしたオブジェクトである。

なお、DRBG の各実装クラスの engineGenerateSeed() はちらっと上で触れたように共通で AbstractDrbg に実装されているのだが、こいつは単純に SeedGenerator.generateSeed() メソッドに委譲しているだけである。

SHA1PRNG.engineNextBytes() の挙動

SHA1PRNG の実装クラスは上でも述べたように sun.security.provider.SecureRandom である。
が、非常にややこしいので以降 SHA1PRNG と記載することにする。

SHA1PRNGengineNextBytes() は、DRBG のケーパビリティが pr_and_reseed 以外の時と(乱数生成のアルゴリズムは全く違うが)ちょっとだけ似ている(気がする)。

どういう事かと言うと、乱数生成器を初期化する際に使用するシード(エントロピー入力)
に別の乱数生成器を内部で使用しているのである。

またこのパターンか。仏の顔も三度まで。いや何か違うな、まだ三度目だし。いやいや、仏の顔も三度は三度目はアウトだ。誰だ「まで」とか付けやがったのは。やっぱり NG だな(NG ではない)。

この内部の乱数生成器は SHA1PRNG のインスタンスであり、クラスの static フィールドに保持されている。この内部インスタンスは生成時に SeedGenerator クラスの getSystemEntropy()generateSeed() の結果をシードとして使用している。やっぱり DRBG のケーパビリティが pr_and_reseed 以外の時と似てね?(気のせいか?)

なお、SHA1PRNGengineGenerateSeed() も単純に SeedGenerator.generateSeed() メソッドに委譲している。(これも DRBG と一緒)

sun.security.provider.SeedGenerator クラス

SeedGenerator クラスは、DRBGSHA1PRNGengineGenerateSeed() の際に実際にシードを生成するクラスである。

このクラスの generateSeed() メソッドは SunEntries.seedSource フィールドで指定されたデバイスからエントロピーデータを読み込んで、そのままシードとして返す。

ただし、指定されたデバイスからの読み込みが出来ない場合には、多数のスレッドを生成して何とかシードを生成しようとする。これが、最初に書いた java.security.egd で指定されたデバイスが読めなかった場合に当たる。

スレッドダンプやフライトレコードで SeedGenerator Thread なるスレッドが沢山いたらコイツの仕業なので java.security.egd の設定を確認した方が良いだろう。

また、このクラスの getSystemEntropy() メソッドは、現在時刻、システムプロパティ、ネットワークアダプタ、その他の環境情報から、20 バイトのエントロピーデータを生成する。

getInstanceStrong() で生成される乱数生成器は何者か

デフォルトコンストラクタではないが、せっかくなので getInstanceStrong() で生成されるインスタンスは何者なのかも調べた。

こいつは セキュリティプロパティ securerandom.strongAlgorithms を読んでインスタンスを生成する。これは「アルゴリズム名:プロバイダ名」をカンマ区切りで連ねたものである。

このプロパティはデフォルト(JDK をインストールした状態)では NativePRNGBlocking:SUN,DRBG:SUN となっている。

つまり普通は NativePRNGBlocking になる。

Discussion