これは、なにをしたくて書いたもの?
Javaの暗号用の乱数ジェネレーター(RNG / Random Number Generator)として、とてもよく使われるSecureRandomですが。
SecureRandom (Java SE 11 & JDK 11 )
ここで指定されるアルゴリズムなどの情報を、あんまりちゃんと見たことがないなぁと思いまして。
1度、調べてみようかと。
ちょっとだけ。
環境
今回の環境は、こちらです。
$ java --version openjdk 11.0.9.1 2020-11-04 OpenJDK Runtime Environment (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04) OpenJDK 64-Bit Server VM (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04, mixed mode, sharing) $ lsb_release -a No LSB modules are available. Distributor ID: Ubuntu Description: Ubuntu 20.04.1 LTS Release: 20.04 Codename: focal $ uname -srvmpio Linux 5.4.0-58-generic #64-Ubuntu SMP Wed Dec 9 08:16:25 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux
Java 11、Ubuntu Linux 20.04です。
SecureRandom
SecureRandomのJavadocを見てみると、次のように書いています。
暗号用に強化された乱数ジェネレータ(RNG)を提供する
強力な暗号化による乱数は、FIPS 140-2, Security Requirements for Cryptographic Modulesのセクション4.9.1に指定されている統計的乱数生成テストに最低限適合しています。
FIPS 140-2, Security Requirements for Cryptographic Modules
さらに、SecureRandomは、非決定論的な出力を生成する必要があります。 したがって、SecureRandomオブジェクトに渡されるシード材料はすべて予測不可能でなければならず、すべてのSecureRandom出力シーケンスは、「RFC 4086: セキュリティのランダム性要件」で説明されているように、暗号的に強くなければなりません。
Randomness Requirements for Security
また、SecureRandomのインスタンスは、特定の乱数生成アルゴリズムを使用して乱数を生成します。
この乱数生成アルゴリズムですが、いくつかの名前こそ見るものの、その一覧ってどこにあるんだろう?と思っていたら、
こちらもJavadocからたどれますね。
Javaセキュリティ標準アルゴリズム名指定 / SecureRandom乱数生成アルゴリズム
SecureRandomで使えるアルゴリズム
では、SecureRandomで使用できるアルゴリズムをもうちょっと見ていきましょう。
Java 11では、以下の7つのアルゴリズムがあるようです。
アルゴリズム名 | 説明 |
---|---|
NativePRNG | ネイティブOSから乱数を取得する。乱数生成のブロック性については何も表明されない |
NativePRNGBlocking | ネイティブOSから乱数を取得し、必要に応じてブロックする |
NativePRNGNonBlocking | ネイティブOSから乱数を取得するが、アプリケーションの速度低下を避けるためにブロックしない |
PKCS11 | インストール済および構成済のPKCS#11ライブラリから乱数を取得する |
DRBG | NIST SP 800-90Ar1で定義されているDRBGメカニズムを使用してSUNプロバイダから提供されたアルゴリズム |
SHA1PRNG | Sunプロバイダが提供する擬似乱数生成(PRNG)アルゴリズム。 このアルゴリズムは、PRNGの基盤としてSHA-1を使用する |
Windows-PRNG | Windows OSから乱数を取得する |
PRNGは、疑似乱数ジェネレータ(Pseudo Random Number Generator)ですね。
次に、こちらのドキュメントを見ていきます。
Java Platform, Standard Editionセキュリティ開発者ガイド, リリース11
各プラットフォームでどのようなアルゴリズムが使えるのかは、JDKプロバイダ・ドキュメントを見ることになります。
SecureRandom実装については、こちら。
Solaris、Linux、macOS、Windowsで使えるアルゴリズムが列挙されており、どのアルゴリズムがどのプロバイダーに
含まれているかが記載されています。
たとえば、LinuxだとSUNプロバイダーによるNativePRNG、DRBG、SHA1PRNG、NativePRNGBlocking、NativePRNGNonBlockingが
利用可能です。
デフォルトではSHA1PRNGが選択され、java.security
のエントロピー収集デバイスをfile:/dev/urandom
またはfile:/dev/random
に
するとNativePRNGが優先されるようです(と書いているのですが、この後で確認してみたら、もう少し複雑でした)。
SecuraRandomに関するプロバイダーは、SUN、SunPKCS11、SunMSCAPIの3つがあるようです。後者2つは、それぞれSolaris、
Windowsで使用されます。
各プロバイダーには、エンジン・クラスと対応するアルゴリズムが含まれています。
エンジン・クラスとは、SecureRandom
やMessageDigest
といったものです。
エンジン・クラスおよび対応するService Provider Interfaceクラス
アルゴリズムはNativePRNG
やSHA-256
といったものですね。
プラットフォームで使えるSecureRandomのアルゴリズムとプロパティを確認する
ここで少し、ソースコードを書いてみましょう。
対象のプラットフォームで使えるSecureRandomのアルゴリズムとプロパティを出力してみます。
こんなソースコードを用意。
PrintSecureRandomAlgorithms.java
import java.security.Provider; import java.security.Security; import java.util.Map; public class PrintSecureRandomAlgorithms { public static void main(String... args) { System.out.println("Platform Supported SecureRandom Algorithms:"); for (String algorithm : Security.getAlgorithms("SecureRandom")) { System.out.printf(" %s%n", algorithm); } System.out.println(); System.out.println("SecureRandom Properties:"); for (Provider provider : Security.getProviders()) { for (Map.Entry<Object, Object> entry : provider.entrySet()) { if (entry.getKey().toString().startsWith("SecureRandom")) { System.out.printf(" [provider: %s] %s = %s%n", provider, entry.getKey(), entry.getValue()); } } } } }
使用しているのは、このあたりですね。
Security (Java SE 11 & JDK 11 )
Provider (Java SE 11 & JDK 11 )
ここでのProviderは、先ほど「SUN」や「SunPKCS11」という名前で出てきたプロバイダーのことです。
Security#getProviders
で、使用できるプロバイダーをすべて取得することができます。
実行してみます。
$ java PrintSecureRandomAlgorithms.java Platform Supported SecureRandom Algorithms: DRBG SHA1PRNG NATIVEPRNGBLOCKING NATIVEPRNGNONBLOCKING NATIVEPRNG SecureRandom Properties: [provider: SUN version 11] SecureRandom.NativePRNG ThreadSafe = true [provider: SUN version 11] SecureRandom.NativePRNGNonBlocking ThreadSafe = true [provider: SUN version 11] SecureRandom.SHA1PRNG = sun.security.provider.SecureRandom [provider: SUN version 11] SecureRandom.NativePRNG = sun.security.provider.NativePRNG [provider: SUN version 11] SecureRandom.NativePRNGNonBlocking = sun.security.provider.NativePRNG$NonBlocking [provider: SUN version 11] SecureRandom.DRBG ImplementedIn = Software [provider: SUN version 11] SecureRandom.SHA1PRNG ThreadSafe = true [provider: SUN version 11] SecureRandom.SHA1PRNG ImplementedIn = Software [provider: SUN version 11] SecureRandom.DRBG = sun.security.provider.DRBG [provider: SUN version 11] SecureRandom.NativePRNGBlocking ThreadSafe = true [provider: SUN version 11] SecureRandom.DRBG ThreadSafe = true [provider: SUN version 11] SecureRandom.NativePRNGBlocking = sun.security.provider.NativePRNG$Blocking
Linux上で動かしているので、使えるアルゴリズムとしてNativePRNG、DRBG、SHA1PRNG、NativePRNGBlocking、
NativePRNGNonBlockingの5つが表示されています。
また、プロパティはSecureRandomのもので絞り込んでいますが、表示されているプロバイダーはSUNだけですね。
SecureRandom内で、どのアルゴリズムが使われる?
次に、SecureRandomのインスタンスで使われるアルゴリズムの決定について見ていきましょう。
SecureRandomのインスタンスを作成するには、コンストラクタを使うかSecureRandom#getInstance
でアルゴリズムを
指定するか、もしくはSecureRandom#getInstanceStrong
で取得することができます。
アルゴリズムを指定した場合はまあいいのですが、その他の方法だとどういうアルゴリズムが選ばれるか気になるところです。
というわけで、こんなソースコードを作って条件を変えながら確認してみます。
PrintSecureRandomInstanceAlgorithm.java
import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public class PrintSecureRandomInstanceAlgorithm { public static void main(String... args) throws NoSuchAlgorithmException { SecureRandom secureRandom; if (args.length > 0 && "strong".equals(args[0])) { secureRandom = SecureRandom.getInstanceStrong(); } else if (args.length > 0) { String algorithm = args[0]; secureRandom = SecureRandom.getInstance(algorithm); } else { secureRandom = new SecureRandom(); } System.out.printf("SecureRandom Algorithm = %s%n", secureRandom.getAlgorithm()); } }
引数でstrong
と指定するとSecureRandom#getInstanceStrong
を使い、それ以外の引数を与えるとアルゴリズムを指定した
ことになり、引数なしだとコンストラクタを使ってSecureRandomのインスタンスを生成します。
確認。
## 引数なし $ java PrintSecureRandomInstanceAlgorithm.java SecureRandom Algorithm = NativePRNG ## SecureRandom#getInstanceStrong $ java PrintSecureRandomInstanceAlgorithm.java strong SecureRandom Algorithm = NativePRNGBlocking ## アルゴリズム指定 $ java PrintSecureRandomInstanceAlgorithm.java DRBG SecureRandom Algorithm = DRBG
このような結果になりました。
SecureRandomコンストラクタを使う
JDKプロバイダ・ドキュメントによると、Linuxでのデフォルトのアルゴリズムは
デフォルトではSHA1PRNGが選択され、
java.security
のエントロピー収集デバイスをfile:/dev/urandom
またはfile:/dev/random
にするとNativePRNGが優先される
ということでした。で、今回はNativePRNGが選択されたことになります。
なので、どこかでjava.security
のエントロピー収集デバイスをfile:/dev/urandom
またはfile:/dev/random
にしているわけですね。
セキュリティ関連のプロパティというと、以下のどちらかで設定することになります。
- java.security.Security#setProperty
$JAVA_HOME/conf/security/java.security
ファイル
今回はセキュリティ関連のプロパティは、$JAVA_HOME/conf/security/java.security
ファイルで見ていくことにしましょう。
$JAVA_HOME/conf/security/java.security
ファイル内の、securerandom.source
を確認してみます。
$ grep securerandom.source /usr/lib/jvm/java-11-openjdk-amd64/conf/security/java.security | grep -v '^#' securerandom.source=file:/dev/random
/dev/random
ですね。
securerandom.source
をコメントアウトしてみましょう。
#securerandom.source=file:/dev/random
確認。
$ java PrintSecureRandomInstanceAlgorithm.java SecureRandom Algorithm = DRBG
SHA1PRNGになると思っていたのですが、DRBGになりましたね…。どうなっているんでしょう?
ソースコードを見てみます。
今は、/dev/urandom
または/dev/random
が使えればNativePRNGとなり、そうでなければDRBGという実装みたいです(?)
デフォルトでどうにも決められない場合は、SHA1PRNGが選択されるようです。
このあたりの話は、こちらに記載があります。
すべてのJava SE実装は、引数なしのコンストラクタnew SecureRandom()を使用してデフォルトのSecureRandomを提供します。このコンストラクタは、登録されているセキュリティ・プロバイダのリスト内を、最も推奨されるプロバイダから順に確認し、その後、SecureRandom乱数ジェネレータ(RNG)アルゴリズムをサポートする最初のプロバイダから新しいSecureRandomオブジェクトを返します。どのプロバイダもRNGアルゴリズムをサポートしていない場合は、SUNプロバイダからSHA1PRNGを使用するSecureRandomオブジェクトを返します。
ここで、securerandom.source
はコメントアウトしたまま、システムプロパティjava.security.egd
で/dev/urandom
を
指定してみます。
$ java -Djava.security.egd=file:/dev/urandom PrintSecureRandomInstanceAlgorithm.java SecureRandom Algorithm = NativePRNG
今度は、NativePRNGになりましたね。
つまり、java.security
のエントロピー収集デバイスというのは、次の2つのいずれかで指定するもののようです。
- システムプロパティ
java.security.egd
- セキュリティ・プロパティ(
java.security.Security#setProperty
または$JAVA_HOME/conf/security/java.security
のsecurerandom.source
)
SecureRandom#getInstanceStrong
を使う
次に、SecureRandom#getInstanceStrong
で使われるアルゴリズムについて見ていきましょう。
こちらについては、ドキュメントに記載があります。
java.security.Securityクラスのsecurerandom.strongAlgorithmsプロパティで定義される強力なSecureRandom実装を取得するには、getInstanceStrong()メソッドを使用します。このプロパティは、重要な値を生成するのに適したプラットフォーム実装をリストします。
$JAVA_HOME/conf/security/java.security
のsecurerandom.strongAlgorithms
プロパティで指定されているようです。
確認してみましょう。
$ grep securerandom.strongAlgorithms /usr/lib/jvm/java-11-openjdk-amd64/conf/security/java.security securerandom.strongAlgorithms=NativePRNGBlocking:SUN,DRBG:SUN
「アルゴリズム:プロバイダー」の形式で書かれている感じがしますね。また、左から優先でしょうか?
それっぽい感じがします。
試してみましょう。左端にNativePRNGを追加。
securerandom.strongAlgorithms=NativePRNG:SUN,NativePRNGBlocking:SUN,DRBG:SUN
確認。
$ java PrintSecureRandomInstanceAlgorithm.java strong SecureRandom Algorithm = NativePRNG
正解のようですね。
左を、間違ったアルゴリズム名にしてみましょう。
securerandom.strongAlgorithms=foo:SUN,bar:SUN,DRBG:SUN
すると、有効なものが選出されます。
$ java PrintSecureRandomInstanceAlgorithm.java strong SecureRandom Algorithm = DRBG
なお、全部向こうなアルゴリズムとプロバイダーの組み合わせにすると、例外になります。
Exception in thread "main" java.security.NoSuchAlgorithmException: No strong SecureRandom impls available: xxxxx
SecureRandom#getInstanceStrong
を使う場合は、どのようなアルゴリズムが選出されうるか、ちゃんと確認しましょう、
という気分になりますね。
file:/dev/urandom
とfile:/dev/random
ここまで来ると、file:/dev/urandom
とfile:/dev/random
の2つがやや気になります。ちょっと調べてみましょう。
RFC 4086にも書いてある、乱数ジェネレーターですね。
/dev/random は、そのプールからのバイト列を返しますが、見積もられるエントロピーがゼロとなるとき、ブロックします。エントロピーが、イベントから、そのプールに追加さえれるに従って、より多くのデータが /dev/random を通じて利用可能になります。このような /dev/random デバイスから得られた乱雑なデータは、十分な乱雑なビット列がそのプール中にある場合、あるいは、合理的な時間が延長されている場合、長期鍵用の鍵生成のために適切です。
/dev/urandom は、/dev/random のように動作します。しかし、これは、たとえ乱雑性のプールについてのエントロピー見積もりがゼロに落ちるときでさえ、データを提供します。これは、セッション鍵生成用、あるいは、より多くの乱雑なビット列を待ち受けるのをブロックすることが許されないような他の鍵生成用に適切である可能性があります。たとえ、そのプールのエントロピーの見積もりが、その過去の出力において小さいときでさえ、データを採り続けることのリスクは、攻撃者が SHA-1 を逆算できるとしたら、現在の出力から計算可能でしょう。SHA-1 が繰り返されないように設計されている場合、これは、合理的なリスクです。
RFC 4086: セキュリティのランダム性要件 / /dev/random デバイス
ざっくり言うと、/dev/random
は安全性が高いけれどブロックする可能性があり、/dev/urandom
は安全性は劣るものの
ブロックしないので性能的には有利、ということみたいです。
ところで、「エントロピー」とは?
源泉については、RFC 4086に記載があります。
RFC 4086: セキュリティのランダム性要件 / エントロピーの源泉
先ほどのRFC 4086の記載の内容からいくと、OS上でのキーボード入力、ディスク関連の割り込み、マウスの動きなどが
源泉となります。ただ、サーバーだとディスク関連の割り込みくらいしか入手できないことになりますが、と。
RFC 4086: セキュリティのランダム性要件 / /dev/random デバイス
SecureRandomのgenerateSeedとnextBytes
JDKプロバイダ・ドキュメントを見ていると、NativePRNGなどにおいて、どのメソッドで/dev/random
が使われるといったことが
書かれています。
具体的には、SecureRandomのgenerateSeed
メソッドとnextBytes
メソッドです。
SecureRandom#nextBytes
メソッドは、ユーザーが指定したバイト数の乱数バイト配列を生成するものです。
SecureRandom#generateSeed
メソッドは、乱数ジェネレーターにシードを与える場合に使用します。
ところで、SecureRandom#generateSeed
メソッドをふだん使っているかというと、微妙なところだと思います。
SecureRandomクラスのJavadocを見ると、自分でシードに関するメソッドを呼び出さないと、シードされないことが書かれています。
SecureRandom (Java SE 11 & JDK 11 )
こういう情報を踏まえて、SecureRandomの各メソッドとアルゴリズム、乱数ジェネレーターの関係を見ていくと良いのでしょうね。
SecureRandomの各アルゴリズムを少し眺める
最後に、SecureRandomの各アルゴリズムを眺めてみましょう。
まあ、簡単な情報の整理とソースコードの記載だけですが。
各アルゴリズムは、SecureRandomSpiクラスのサブクラスとして作成されています。
SecureRandomSpi (Java SE 11 & JDK 11 )
SecureRandomSpiクラスは、SecureRandomクラスが内部で使用しているものです。
各プラットフォームのアルゴリズムの実装は、こちらにあります。
NativePRNG、NativePRNGBlocking、NativePRNGNonBlocking
最初は、NativePRNG、NativePRNGBlocking、NativePRNGNonBlockingから。
これらは、ネイティブOSから乱数を得るアルゴリズムということでした。
ドキュメントを見ると、このように書かれています。
- NativePRNG
nextBytes
…/dev/urandom
generateSeed
…/dev/random
- NativePRNGBlocking
nextBytes
…/dev/random
generateSeed
…/dev/random
- NativePRNGNonBlocking
nextBytes
…/dev/urandom
generateSeed
…/dev/urandom
NativePRNGBlockingはnextBytes
、generateSeed
を問わずブロックする可能性がある/dev/random
を使用し、
NativePRNGNonBlockingはブロックしない/dev/urandom
を使います。
NativePRNGはnextBytes
はブロックしない/dev/urandom
を使い、generateSeed
ではブロックする可能性がある/dev/random
を
使用します。
鍵などの用途で使う場合はNativePRNGBlockingを使い、そうでない場合はNativePRNGNonBlockingを使うのが良さそうでしょうか。
nextBytes
メソッドのみを使う場合は、NativePRNGとNativePRNGNonBlockingに差はなさそうですね。
この3つは、同じソースコード中に収められています。
NativePRNG。
NativePRNGBlocking。
NativePRNGNonBlocking。
実際に、各乱数ジェネレーター(/dev/random
など)をどう割り当てているかは、こちらを見ればよいでしょう。
なお、NativePRNGのgenerateSeed
についてはエントロピー収集デバイスの指定で調整可能なようです。
SHA1PRNG
SHA1PRNGの説明はこうでした。
Sunプロバイダが提供する擬似乱数生成(PRNG)アルゴリズム。 このアルゴリズムは、PRNGの基盤としてSHA-1を使用します。 各操作につき値が1増加する64ビット・カウンタを使って鎖状につながった真にランダムなシード値から、SHA-1ハッシュを計算します。 160ビットのSHA-1出力のうち、64ビットだけが使用されます。
どのプラットフォームでも使えることが特徴です。
実装としては、こちらのようです。
アルゴリズムを登録しているところ。
ちなみに、SHA1PRNGは暗号学的に安全ではないため、避けた方がよさそうですね。
Google Developers Japan: Android N で廃止されるセキュリティ「Crypto」プロバイダ
NativePRNG系のものが使えないWindowsだと、DRBGを使った方が良いのでは?
DRBG
JEP 273: DRBG-Based SecureRandom Implementations
ドキュメントによるとこのような説明になっています。
NIST SP 800-90Ar1で定義されているDRBGメカニズムを使用してSUNプロバイダから提供されたアルゴリズム
ソースコードはこちら。
また、プロバイダーの説明には、以下のように書かれています。
次のメカニズムとアルゴリズムがサポートされています。SHA-224、SHA-512/224、SHA-256、SHA-512/256、SHA-384およびSHA-512を使用するHash_DRBGおよびHMAC_DRBG。AES-128、AES-192およびAES-256を使用するCTR_DRBG (導出関数を使用する場合と使用しない場合がある)。各組合せでサポートされている予測耐性と再シード、およびセキュリティ強度は、112から、それがサポートする最大強度まで要求できます。
この説明からして、単に「DRBG」と指定する以外にも調整可能な項目がありそうですね。
このあたりの要件に合わせて調整可能なことが、DRBGのポイントのようです。
こちらに、securerandom.drbg.config
というプロパティの説明があります。
securerandom.drbg.configは、java.security.Securityクラスのプロパティです。
というわけで、$JAVA_HOME/conf/security/java.security
ファイルを確認してみます。
securerandom.drbg.config=
項目としては存在しますが、値が空ですね。
その上のコメントを見ると、設定方法が書かれています。
# Examples, # securerandom.drbg.config=Hash_DRBG,SHA-224,112,none # securerandom.drbg.config=CTR_DRBG,AES-256,192,pr_and_reseed,use_df # # The default value is an empty string, which is equivalent to # securerandom.drbg.config=Hash_DRBG,SHA-256,128,none #
デフォルトの値は、こちらです、と。
securerandom.drbg.config=Hash_DRBG,SHA-256,128,none
メカニズム(Hash_DRBG、CTR_DRBG、Hmac_DRBG)、アルゴリズム(SHA-256など)、強度(112〜)などが指定できます。
構文。
# aspect: # mech_name | algorithm_name | strength | capability | df
メカニズム。
# // The DRBG mechanism to use. Default "Hash_DRBG" # mech_name: # "Hash_DRBG" | "HMAC_DRBG" | "CTR_DRBG"
# // The DRBG algorithm name. The "SHA-***" names are for Hash_DRBG and # // HMAC_DRBG, default "SHA-256". The "AES-***" names are for CTR_DRBG, # // default "AES-128" when using the limited cryptographic or "AES-256" # // when using the unlimited. # algorithm_name: # "SHA-224" | "SHA-512/224" | "SHA-256" | # "SHA-512/256" | "SHA-384" | "SHA-512" | # "AES-128" | "AES-192" | "AES-256"
強度。
# // Security strength requested. Default "128" # strength: # "112" | "128" | "192" | "256"
予測耐性と再シード。
# // Prediction resistance and reseeding request. Default "none" # // "pr_and_reseed" - Both prediction resistance and reseeding # // support requested # // "reseed_only" - Only reseeding support requested # // "none" - Neither prediction resistance not reseeding # // support requested # pr: # "pr_and_reseed" | "reseed_only" | "none"
メカニズムがCTR_DRBGの時に、導出関数を使用するかどうか。
# // Whether a derivation function should be used. only applicable # // to CTR_DRBG. Default "use_df" # df: # "use_df" | "no_df"
プロパティに指定した項目をパースしているのは、このあたりですね。
Linux環境下では、SecureRandom#getInstanceStrong
のデフォルト設定がこうだったこともあり、NativePRNG系のものが
使える状態であれば、そちらでもいいかな?
securerandom.strongAlgorithms=NativePRNGBlocking:SUN,DRBG:SUN
PKCS11
PKCS11ライブラリより、乱数を取得するアルゴリズムです。
ソースコードはこちら。
あんまり深追いしません…。
Windows-PRNG
Windows OSから乱数を取得するアルゴリズム、となっていますが…Windows-PRNGというものはなく、実体はSunMSCAPI
だったりします。
Windows用のNativePRNGのクラスはありますが、実体がありません。
こちらも、あんまり深追いしません…。
まとめ
これまであまり見てこなかった、SecureRandomのアルゴリズムについて、ちょっと追ってみました。
用語などとちゃんと向き合っていなかったので、かなり苦労しましたが…いろいろ勉強になりましたねぇ。
今度から、しっかり見るようにしましょう…。
参考
JCA (Java 暗号化アーキテクチャ)使い方メモ - Qiita
#JJUG Java における乱数生成器とのつき合い方 - Speaker Deck
Java における乱数生成器とのつき合い方 / #JJUG CCC 2019 fall に登壇してきました - k11i.biz