SecureRandom クラスと java.security.egd
背景
最近 java.security.SecureRandom
クラスについていろいろ確認したので自分用の備忘録を兼ねて。
なお調べたのは Java 17 で Linux の場合。他のバージョンや OS だと異なったりするので注意。
一応 JDK のソースを見ながら確認したつもりだが、もし誤りがあればツッコミ頂けるとありがたい。
ちなみに、きっかけは SecureRandom
のデフォルトコンストラクタで生成されたインスタンスが java.util.UUID.randomUUID()
で使われていたから。
Tomcat のセッション ID 生成にも使われてるが、こちらはアルゴリズム指定のコンストラクタだった。(こちらのアルゴリズムのデフォルトは SHA1PRNG
)
ざっくりした結論
普通の環境で SecureRandom
をデフォルトコンストラクタで生成すると、以下のようになる。
- システムプロパティ
java.security.egd
が設定されていない場合、および、java.security.egd
がfile:/dev/random
に設定されている場合- アルゴリズムは
NativePRNG
になる。 -
generateSeed()
メソッドは/dev/random
から読みだす。 -
nextBytes()
メソッドは/dev/urandom
から読みだす。(が、読んだ結果がそのまま返るわけではない)
- アルゴリズムは
-
java.security.egd
がfile:/dev/urandom
に設定されている場合- アルゴリズムは
NativePRNG
になる。 -
generateSeed()
メソッドは/dev/urandom
から読みだす。 -
nextBytes()
メソッドは/dev/urandom
から読みだす。(が、読んだ結果がそのまま返るわけではない)
- アルゴリズムは
-
java.security.egd
が上記以外の URL に設定されていて、ちゃんと読める場合- アルゴリズムは
DRBG
(パラメータはHash_DRBG,SHA-256,128,none
) になる。 -
generateSeed()
メソッドは指定されたデバイスから読みだす。 -
nextBytes()
メソッドはパラメータに従って生成する。
- アルゴリズムは
-
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
は何になっているかと言うと、
-
NativePRNG
が使用可能で、かつ、sun.security.provider.SunEntries
クラスのseedSource
フィールドが文字列としてfile:/dev/urandom
かfile:/dev/random
のいずれかと等しい場合
⇒NativePRNG
- 上記以外の場合
⇒DRBG
そう、最初に書いたようなデフォルトコンストラクタでのアルゴリズムの決定方法はココから来ている。
ちなみに、何らかの理由により上記の 2 つも含めて、設定されているセキュリティプロバイダのいずれの乱数生成器も使えない場合には SUN
プロバイダの SHA1PRNG
が使用される。(まぁ普通そんな事にはならないと思うが)
SunEntries.seedSource
フィールドの設定内容
SunEntries.seedSource
がどのように設定されているかを正確に書くと、以下のようになる。
- システムプロパティ
java.security.egd
が設定されている場合
⇒java.security.egd
の設定値 - それが設定されていなくて、かつ、セキュリティプロパティ
securerandom.source
が設定されている場合
⇒securerandom.source
の設定値 - それも設定されていない場合
⇒ 空文字列
ちなみに、セキュリティプロパティ 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
とは?
Native
な PRNG
と言う事だろう。
Native
と言うのは、多分 OS のデバイスから取得したエントロピーソースをメインで使用していると言う事を表しているのだと思う(勘でモノを言っています)。
PRNG
は Pseudo Random Number Generator。
疑似(Pseudo)乱数(Random Number)生成器(Generator)。
ホンモノの乱数じゃないから Pseudo と言う事なんだと思うが、NativePRNG
もニセモノって事なのだろうか。やっぱり良く分からんな。
NativePRNG
の実装クラスは sun.security.provider.NativePRNG
である。
NativePRNG
が使用可能とはどういうことか
NativePRNG
が使用可能とはどう言う事かと言うと、以下の 2 つの条件を満たしていると言う事である。
- シード用デバイスが読み出し可能である。
ここで「シード用デバイス」は基本的に上記のSunEntries.seedSource
フィールドの設定値である。
しかし、当該フィールドがfile
プロトコルじゃない場合、あるいは、file
プロトコルであっても読み出しができない場合は/dev/random
にフォールバックする。
つまり、最悪でも/dev/random
が読み出し可能であれば OK という事である。 - 乱数用デバイスが読み出し可能である。
ここで「乱数用デバイス」は/dev/urandom
固定である事に注意が必要である。プロパティ等で変更することは出来ない。
これらからすると、普通の Linux 環境で NativePRNG
が使用可能ではないと言う事はそうそう起きないのではないかと思う。
NativePRNG.engineNextBytes()
の挙動
最初に書いたように、NativePRNG
の場合でも engineNextBytes()
は乱数用デバイス(つまり /dev/urandom
)から読んだ値そのものを返しているわけではない。
じゃあ何を返しているかと言うと、以下の 2 つからそれぞれ要求されたバイト数分だけ取得して、それらの結果を XOR して返している。
-
/dev/urandom
から読みだしたデータ - インスタンス内部に持っている乱数生成器で生成した乱数
ややこしいが 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/random
と file:/dev/urandom
以外を設定するとデフォルトコンストラクタでは NativePRNG
は使われなくなるが)
ちなみに、NativePRNG
の engineGenerateSeed()
の方は上で触れた「シード用デバイス」から読みだしたものそのままを返している。
NativePRNGBlocking
と NativePRNGNonBlocking
デフォルトコンストラクタでは生成され得ないが、NativePRNG
の仲間に NativePRNGBlocking
と NativePRNGNonBlocking
と言うアルゴリズムもある。
これらの実装クラスは NativePRNG
のネストしたクラスとなっていて、それぞれ sun.security.provider.NativePRNG.Blocking
と sun.security.provider.NativePRNG.NonBlocking
である。
これらは NativePRNG
と挙動はほぼ一緒だが「シード用デバイス」と「乱数用デバイス」だけが異なる。
-
NativePRNGBlocking
シード用デバイス:/dev/random
乱数用デバイス:/dev/random
確かにどちらもブロックするデバイスだ。 -
NativePRNGNonBlocking
シード用デバイス:/dev/urandom
乱数用デバイス:/dev/urandom
確かにどちらもブロックしないデバイスだ。
ちなみに、システムプロパティ java.security.egd
を file:/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 だ。(内容は全く分かっていない)
- メカニズム
Hash_DRBG
、HMAC_DRBG
、CTR_DRBG
のいずれか。
Hash
はそのままハッシュ、HMAC
は Hash-based Message Authentication Code、CTR
はカウンタ、と言う事でよろしいか?
デフォルトはHash_DRBG
。 - アルゴリズム
- メカニズムが
Hash_DRBG
、HMAC_DRBG
の場合、以下のいずれかを使用可能。
SHA-224
、SHA-512/224
、SHA-256
、SHA-512/256
、SHA-384
、SHA-512
よく見かけるやつだな(知ってるとは言ってない)。
デフォルトはSHA-256
。 - メカニズムが
CTR_DRBG
の場合、以下のいずれかを使用可能。
AES-128
、AES-192
、AES-256
これもよく見かけるやつだな(知ってるとは言ってない)。
デフォルトはAES-256
。
- メカニズムが
- セキュリティ強度
112
、128
、192
、256
のいずれか。
この数字は bit か?
デフォルトは128
。 - ケーパビリティ
none
、reseed_only
、pr_and_reseed
のいずれか。
reseed
は途中でシードを設定できる、と言う感じか。pr
はprediction resistance
らしい。
デフォルトはnone
。 - 導出関数(derivation function)
メカニズムがCTR_DRBG
の時のみ意味がある。
use_df
、no_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()
のシード生成方法が変わるわけではない事に注意。
- ケーパビリティが
pr_and_reseed
の場合
sun.security.provider.SeedGenerator
クラスのgenerateSeed()
を使用する。 - ケーパビリティが
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
と記載することにする。
SHA1PRNG
の engineNextBytes()
は、DRBG
のケーパビリティが pr_and_reseed
以外の時と(乱数生成のアルゴリズムは全く違うが)ちょっとだけ似ている(気がする)。
どういう事かと言うと、乱数生成器を初期化する際に使用するシード(エントロピー入力)
に別の乱数生成器を内部で使用しているのである。
またこのパターンか。仏の顔も三度まで。いや何か違うな、まだ三度目だし。いやいや、仏の顔も三度は三度目はアウトだ。誰だ「まで」とか付けやがったのは。やっぱり NG だな(NG ではない)。
この内部の乱数生成器は SHA1PRNG
のインスタンスであり、クラスの static
フィールドに保持されている。この内部インスタンスは生成時に SeedGenerator
クラスの getSystemEntropy()
と generateSeed()
の結果をシードとして使用している。やっぱり DRBG
のケーパビリティが pr_and_reseed
以外の時と似てね?(気のせいか?)
なお、SHA1PRNG
の engineGenerateSeed()
も単純に SeedGenerator.generateSeed()
メソッドに委譲している。(これも DRBG
と一緒)
sun.security.provider.SeedGenerator
クラス
SeedGenerator
クラスは、DRBG
と SHA1PRNG
の engineGenerateSeed()
の際に実際にシードを生成するクラスである。
このクラスの generateSeed()
メソッドは SunEntries.seedSource
フィールドで指定されたデバイスからエントロピーデータを読み込んで、そのままシードとして返す。
ただし、指定されたデバイスからの読み込みが出来ない場合には、多数のスレッドを生成して何とかシードを生成しようとする。これが、最初に書いた java.security.egd
で指定されたデバイスが読めなかった場合に当たる。
スレッドダンプやフライトレコードで SeedGenerator Thread
なるスレッドが沢山いたらコイツの仕業なので java.security.egd
の設定を確認した方が良いだろう。
また、このクラスの getSystemEntropy()
メソッドは、現在時刻、システムプロパティ、ネットワークアダプタ、その他の環境情報から、20 バイトのエントロピーデータを生成する。
getInstanceStrong()
で生成される乱数生成器は何者か
デフォルトコンストラクタではないが、せっかくなので getInstanceStrong()
で生成されるインスタンスは何者なのかも調べた。
こいつは セキュリティプロパティ securerandom.strongAlgorithms
を読んでインスタンスを生成する。これは「アルゴリズム名:プロバイダ名」をカンマ区切りで連ねたものである。
このプロパティはデフォルト(JDK をインストールした状態)では NativePRNGBlocking:SUN,DRBG:SUN
となっている。
つまり普通は NativePRNGBlocking
になる。
Discussion