JavaのSecureRandomで乱数生成するとブロックされるの?について
まずざっくり、JavaのSecureRandomのアルゴリズムが、NativePRNGやNativePRNGBlocking, NativePRNGNonBlockingだと、osの(疑似)乱数生成器を使って、seed(getSeed)を作ったり乱数生成(nextBytes)したりする
ホントは他にも色々アルゴリズムあるけど、LinuxOSの(疑似)乱数生成器のブロック動作がこの記事で書きたいことなので、他のアルゴリズムについては扱わない
SecureRandomのアルゴリズムの調べ方
code: java
new java.security.SecureRandom().getAlgorithm()
どの乱数生成器使うのかの指定
システムプロパティ java.security.egd
$ jcmd <PID of JVM> VM.system_properties | grep java.security
セキュリティプロパティ
$JAVA_HOME/conf/security/java.security の securerandom.source の設定
これの設定によって、デフォルトで採用されるアルゴリズムも変わってくるぞと
なお、もうちょい先のJavaだと -XshowSettings:security オプションで上記のような確認が簡単にできるようになるかも
OSがLinuxの場合(疑似)乱数生成器として、/dev/random or /dev/urandom が使われる
/dev/random の方は、ざっくりいうとOSが集めた環境ノイズを消費して乱数を生成してくれる。が、この環境ノイズが枯渇するとノイズが貯まるまで処理をブロックしてしまうことがある。
環境ノイズはハードウェア(ドライバ)経由で取得したりする
ちなみにLinuxはCPUのRDRAND命令とかには依存しないようになってるっぽい
/dev/urandom の方も環境ノイズを使う点は同様だけど、ノイズが枯渇しても今ある分のノイズを使ってなんとか乱数を生成する。/dev/randomのようにノイズが貯まるまで待たないので、当然生成される乱数の精度は /dev/random に比べて落ちる
ただし、最近のLinuxのアーキテクチャだと /dev/randam 使っててもあんまブロックされないかも
ChaCha20-DRNG では Linux-Legacy-RNG と異なり、ブロッキングプールが実は廃止された。
ChaCha20-DRNG でも、Linux-Legacy-RNG と同じようにエントロピーを貯める入力プールがある。ただ、Linux-Regecy-RNG の時のようなブロッキングプール、非ブロッキングプールといったプールは存在せず、 /dev/random、 /dev/urandom は共通の一つの状態を参照する。状態は固定長のビット列領域で、ChaCha20 の乱数ストリーム生成に使用される。状態ははじめエントロピープールから生成されたシードで初期化され、その後は入力プールが更新されたかによらず入力プールの内容と状態のデータを使って状態を更新し続けながら、乱数生成をしていく。もちろん、入力プールに環境ノイズが入ればそれは都度反映されるし、環境ノイズが入らなくても暗号学的に安全な乱数生成が行われることになる。これにより、 /dev/random と /dev/urandom の違いは、一番最初のシード初期化が完了するまで待つか、それを待たずに警告のみでエントロピーの反映が甘いかもしれない乱数を警告込みでブロックしないで生成するかの違いになった。シード初期化自体も基本的には1秒ほどの短時間で終わるため、ほとんどの場合 /dev/random と /dev/urandom の違いを意識して使用する必要は無くなったわけだ。
Linuxの(疑似)乱数生成器の入力プールの監視方法
/proc/sys/kernel/random/poolsize
入力プールのビットサイズ
プールの容量。プールにどれだけ水が入るのか。
code: sh
$ cat /proc/sys/kernel/random/poolsize
4096
/proc/sys/kernel/random/entropy_avail
入力プールのエントロピーサイズ
プールにどれだけ水がはいっているのか。
code: sh
$ cat /proc/sys/kernel/random/entropy_avail
3231
entropy_availはpoolsizeを超えることはなく、entropy_availがpoolsizeに近ければ近いほど潤沢にノイズが貯まっているという話になる。言うまでもなく、entropy_availが0に近ければ相対的に貯まっているノイズ量は少ない。
LinuxのOSのレベルで上記のような話があるがJavaのNativePRNGの初期化の実装を見ると以下の感じになってる (OpenJDK11のソースより)
NativePRNGの場合はVariantはMIXED
seed生成時 /dev/random, nextBytes(SecureRandomでの乱数生成)時は /dev/urandom
で、seedの生成は基本初回のみになるので、ブロックされうる /dev/random を参照するのは初回の初期化時のみ
なので、Javaの実装レベルでもSecureRandomの乱数生成で毎回ブロックされうるかというとそうでもない気がする
もちろんこの辺はJavaの実装とかにも寄ってくるはず
ちなみにアルゴリズムがNativePRNGBlocking(VariantはBLOCKING)だと、seed生成時, nextBytes時どちらも /dev/random が使われるのでブロックされる可能性は高くなるよ
で、NativePRNGNonBlockingは、seed生成時, nextBytes時どちらも /dev/urandom が使われるので、ブロックされないけど乱数の精度は相対的に落ちる
OpenJDKの場合上記のような実装になってそうだが、JavaのSpecification的には /dev/random 使ってるとブロックされる可能性があるよということになっている
ノート: 実装によっては、エントロピ・ソースがさまざまなUnixライクなオペレーティング・システム上で/dev/randomである場合など、generateSeed、reseedおよびnextBytesメソッドは、エントロピが収集されるにつれてブロックされる可能性があります。
用途次第なところもあると思うけど、他の代替手段があったりするなら、上記のカタログスペックを受けて安全側に倒す形(ブロックされうるという前提)で、他の代替手段を採用するみたいな判断もありうると思う
まとめ
OSのレベルと、Javaのレベルで調べていくと最近は問題になるようなブロックのされ方はあんましないような感じもする
が、カタログスペック上は /dev/random つかってるとブロックされる可能性はあるというはなしになるので、この辺考慮にいれてどうするか判断するとよいと思う
また、用途に応じてどの程度の暗号強度が必要かみたいな話とかもあるので、そのあたりのバランスもとりながらどうするか決めていくのがよいと思う