lycheejam's tech log

チラ裏のメモ帳 | プログラミングは苦手、インフラが得意なつもり。

C# 乱数の生成(RNGCryptoServiceProviderを使う)

概要

前回、こういった乱数の生成記事を書きましたがめちゃくちゃ間違っていたので
ちゃんとしたものを書きました。恥ずかしい限りです。
kitigai.hatenablog.com

ソース

github.com

System.Randomクラスを利用する

上記のパスワード生成機ではSystem.Random.Nextを使用して
文字列の中からランダムに指定されたIndexの文字をパスワードの文字列に結合しています。

その際に、Randomにseed値を渡してやる必要があります。
このシード値を元に計算を開始します。そのため、このシード値が計算のたびに同じものだと同じ値が返されてしまいます。

public string CreatePasswd(int pwdLen, string pwdChar, int seed) {
    var sr = new StringBuilder(pwdLen);
    var rnd = new Random(seed);

    for (int i = 0; i < pwdLen; i++) {
        sr.Append(pwdChar[rnd.Next(pwdChar.Length)]);
    }
    return sr.ToString();
}

修正前(DateTime.Now.Ticksを使用)

冒頭でも紹介した乱数の生成記事で言っている方法です。
乱数と言うより、カウントアップし続けている時計ですね。

これを下記のように使用していました。

Random r = new Random((int)DateTime.Now.Ticks);

アホなので何も気が付かなかったのですがlong型からint型にキャストしたら普通に桁あふれしますよね。
何故か頭の中で上位ビットが切り捨てられて下位ビットのみ格納されてると思いこんでいました。
そしたら2の補数計算とかズレますよね。

DateTime.Now.Ticksを使用した正しいシード値の方法

上記の方法をとるなら下記のコードが正しい

Random r = new Random((int) DateTime.Now.Ticks & 0x0000FFFF);

ビット演算を行って上位ビットを切り捨てる必要がありました。
(まあこのビット演算公式ドキュメントに載っているのにまったく気が付かなかったんですけどね)

修正後(RNGCryptoServiceProviderを使用)

RNGCryptoServiceProvider.GetByteを使用してバイト配列に乱数を取得します。

public int CreateRandomSeed() {
    var bs = new byte[4];
    //Int32と同じサイズのバイト配列にランダムな値を設定する
    using (var rng = new RNGCryptoServiceProvider()) {
        rng.GetBytes(bs);
    }
    //RNGCryptoServiceProviderで得たbit列をInt32型に変換してシード値とする。
    return BitConverter.ToInt32(bs, 0);
}

ソースはdobon.netさんからコピペです。

  1. RNGCryptoServiceProvider.GetByteで各バイト配列に乱数を取得
  2. BitConverter.ToInt32でバイト配列をInt32型に変換

こんな感じで乱数が出来上がりました。これをシード値に使ってやる。

乱数の生成アルゴリズムの過程で乱数に偏りが出るようですが
調べても難しい数式ばかりで諦めました。

雑感

このコードを書いたのが結構前で思い出すのに時間がかかりました。
なんかもっと調べてたはずなんですけど忘却の彼方です。

調べたらすぐ忘れないように記事にしておいたほうがいいですね。