過去に作ったプログラムで、あまり良くない乱数の実装をしていることがわかったので、メモしておく。
今回気づいたのは、過去に作った、ひたすらランダムな質問に回答していくようなWebサービスである。質問を発行した時に同時にトークンとなるランダムな文字列も発行しておき、質問に回答する時にそのトークンを送ることでその回答が正当なものであると承認するという仕組みだ。
こうすることで、他人に横から勝手に回答されないようにしたり、存在しない質問を捏造して回答できないようにしている。
そのトークンを作る実装がこんな感じだった(Go)。
bytes := []byte(question1.ID + question2.ID + fmt.Sprint(time.Now().UnixNano()))
tokenString := fmt.Sprintf("%x", md5.Sum(bytes))
質問のID2つに時間を加え、それをハッシュ化したものをトークンとしている。まあ、なんでも良いのだが、質問自体がランダムに選ばれるので、ランダムっぽい文字列をベースにすればいいと安易に思っていた。
ナノ秒単位で同じ時刻に、同じ質問2つが選ばれて重複する可能性はきわめて低いので、普通に使っていれば重複することはないと思った。
だが、そもそも質問をランダムに選ぶための乱数が時刻をシード値にしているので、時刻にあたりをつけて総当りすればどの質問が選ばれるか、最終的にどんなハッシュ値になるかを予測することが可能だ。この実装は危険だということに今更気づいた。
質問を横取りされるだけなので、大したセキュリティリスクではないが、これがログイン式のサービスのアクセストークンやセッションIDだと大変なことになるので、別の方法を考えなければならない。
普通に math/rand
で乱数を作ると、シード値が時間である以上、同じ現象が起こる。
どうしたらいいかというと、GoでアクセストークンやセッションIDを生成する時は、 crypto/rand
というモジュールを使うのがおすすめ。
Unix には /dev/urandom
という乱数を生成するデバイスファイルが用意されている。ハードウェア的なノイズなどをベースに乱数を作っているので、同じ時間に実行したからといって同じ値になったりすることはない。
試しにこのファイルを cat してみるとランダムな文字列で画面が埋め尽くされるはず。
実際に修正してみたコードはこんな感じ。
bytes := make([]byte, 64)
rand.Read(bytes)
tokenString := fmt.Sprintf("%x", md5.Sum(bytes))
絶対に重複しないよう64バイトにしてみたが、それなりに重い処理らしいので、ほどほどの数にしておくのが良い気がする。