PHPで本気で安全なパスワードを生成する
PHPで、いわゆる安全な秘密鍵を生成する方法っていうのがネットにほとんど記述されてないという事実に愕然としている。みんな、暗号乱数ぐらい勉強してくれ・・・。
— Takashi Kawasaki (@espresso3389) April 12, 2014
なんて発言をしたら結構反響があったので、責任を取って、これならまぁ許せるというサンプルを出しておく。エントロピーの無駄遣いなどというぐらいの理解がある人は自分で実装してください。マジ実装すると結構面倒なので諦めました。
コピペするならここからどうぞ
<?php // パスワードに使っても良い文字集合 $password_chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; $password_chars_count = strlen($password_chars); // $sizeに指定された長さのパスワードを生成 function generate_password($size) { global $password_chars; global $password_chars_count; $data = mcrypt_create_iv($size, MCRYPT_DEV_URANDOM); $pin = ''; for ($n = 0; $n < $size; $n ++) { $pin .= substr($password_chars, ord(substr($data, $n, 1)) % $password_chars_count, 1); } return $pin; } print generate_password(32); ?>
お言葉の割にはショボいコードですねと仰る方はとりあえず、考察をどうぞ。
考察(1)
乱数の元はやはり、暗号用の疑似乱数(Cryptographically Secure Pseudo Random Number Generator: 暗号論的擬似乱数生成器)、CSPRNGを利用しないといけません。
我が日本が誇るスーパー乱数 mt_rand (メルセンヌ・ツイスター)は「ちょっと」違うんですが、一般論として、普通の乱数は、線形合同法と言われる方法で作られており、所詮は、一次関数の漸化式でしかなく、初期値(seed)が同じであれば、何度でも同じ乱数列を発生することが出来るからです。さらに、周期性があり、出てくる数字を観測していくことによって、原理的には、次に出てくる数字を予想することが可能になります。
さらに、srandと言われる、初期値(seed)を設定する関数に対して、時刻を使って初期化するようなプログラムも沢山ありますが、これは、プログラムを実行する時間によって出てくる数字が決定されてしまうので、これまた、出てくる数字を予想することが簡単になる問題を孕んでいます。
PHP: mt_srand - Manual などには、事もなさげに、make_seedなんて関数を定義してくれていますが、これを真に受けて、次の様なコードを書いちゃいけません。
<?php // // 警告:危険:絶対にまねしちゃダメ!!!!!!!!!!!!!!!!!! // // マイクロ秒でシードを指定します function make_seed() { list($usec, $sec) = explode(' ', microtime()); return (float) $sec + ((float) $usec * 100000); } // 真似しちゃいけないよん ... // $sizeに指定された長さのパスワードを生成 function generate_password($size) { mt_srand(make_seed()); // ダメーダメー絶対ダメー $pin = ''; for ($n = 0; $n < $size; $n ++) { $pin .= substr($password_chars, mt_rand(0, $password_chars_count-1), 1); } return $pin; } print generate_password(32); ?>
このコードを少しでもマシにしたいなら、本当の乱数とは(弱い乱数・強い乱数・真の乱数)で紹介されているコードのように、
<?php ... // ちゃんとCSPRNGを使って初期化します function make_seed() { return hexdec(bin2hex(openssl_random_pseudo_bytes(4))); } ... ?>
とやるべきでしょう・・・・。こっちの方が短いし、お手軽。
いや、これでも、ちょっと気になるんですけど。
パスワードの安全性とは
というのも、パスワードの強度、安全性というのは、何回、パスワードを試したら、パスワードがバレてしまうかという回数で表現されるんですが、簡単に言えば、0000~9999の中からパスワードを選べと言われても、10000回繰り返したらどっかでパスワードにヒットします。
4桁の数字だと、結構な割合の人が誕生日とか設定してしまうので、実際には、365回試せば、ヒットしちゃうでしょう。
もっというと、毎日が多分、誰かの誕生日なので、世界中の誰かのパスワードで良いなら、「1108」とか適当な数字を言っても多分当たります。
話を元に戻すと、
hexdec(bin2hex(openssl_random_pseudo_bytes(4)))
で得られる数字は、0~4294967295 (2の32乗-1)です。このくらいしか総数がないと、今時のコンピュータなら頑張れば、数時間でパスワードを探せてしまいます。
もっとデカい数字の方が良いわけです。2の64乗とか、2の128乗とか。でも、PHPだとデカい数字使うのはかなり面倒だと思う。いや、どんな言語使ってもかなり面倒。素人が数時間でさくっと作れるレベルではありません。かけ算とか実装しようとすると、フーリエ変換ぐらいは分からないとどうしようもありません。
ということで、さっさと最初のコードをコピペしてください。
考察(2) エントロピー使いすぎ
乱数の元になる、訳分からん具合、混沌具合(エントロピー)は、大事なものです。
パスワードを予想しようとするような相手を攪乱させるためには、この混沌具合をなるべく大きくしたいわけですが、実のところ、このグチャグチャを作るのには、理路整然としたコンピュータは向いていません。
そのため、専用の回路を作ったり、あるいは、いろんなランダムな要因(キーボード叩かれるタイミングとか、マウスが動いたタイミングとか、その他、いろいろ)を使うんですが、これらも、ユーザーが介在しないマシンだと限られていたりします。
要は、ランダムな要因が足らなくなるわけです。そのランダム具合が足らなくなると、コンピュータは、乱数としてはあんまり質の良くない乱数で誤魔化したり、あるいは、乱数を発生してくれなくなったりします。
にも関わらず、最初の例では、n文字のパスワードを作成するために、nバイトの乱数を要求しています。ここは、本来であれば、アルファベット(大文字・小文字)+数字の62文字を使ってでn文字のパスワードを作るのならば、0.74*nバイトで十分なはずです(log62/log256)。25%もの乱数が無駄に使われています。頑張ればもっとマシな実装になるはずですが、マジ実装すると、むしろ遅くなるかも知れません。誰か頑張ってみて。
考察(3) openssl_random_pseudo_bytes vs. mcrypt_create_iv
さっきの例では、 mcrypt_create_iv を使っていますが、ほぼ同じ事が出来る関数として、 openssl_random_pseudo_bytes というものもあります。これらについては、
openssl_random_pseudo_bytes と mcrypt_create_iv の比較
http://blog.sarabande.jp/post/38227420927
などが参考になるかも知れませんし、ならないかも知れません。結果的には好きな方を使ってみろということかと。ほとんどの場合、変わらないでしょう。
Ruby好き好き@novさんからは、
@espresso3389 phpseclibのこれ https://t.co/Pb3P0Gkrte
— nov matake (@nov) April 12, 2014
なんてものを教えて貰いましたが、これは、どの関数使ったら良いんじゃコンニャローっていうのを良きに計らってくれるようです。
考察(4) /dev/random vs. /dev/urandom
UNIXには、暗号に使っても大丈夫なCSPRNGとして、/dev/random, /dev/urandomというデバイスファイルが用意されています。このファイルからデータを読み出すと、乱数列が返ってきます。
ただし、これはさっきのエントロピーを使いすぎると、乱数を発生してくれなくなる奴(/dev/random)と、エントロピー使いすぎると、雑になる奴(/dev/urandom)です。
こういう仕様なので、/dev/random は使いにくすぎます。多分、普通のスキルの人にこっちが使いこなせるとは思えません。
なので、/dev/urandomはどんくらい信用できるんだよっていう話になるわけですが、正直、大丈夫そうです。というか、こいつにそっぽ向かれると、他に使えるものがないというのが実情なので、諦めてこれを使うしかありません。
ということで、最初のコードの mcrypt_create_iv には、 /dev/urandom を使えよという指示、 MCRYPT_DEV_URANDOM が指定されています。
あと、こやつらは「遅い」という問題もありますが、もう、そんなことまで言われても知りません・・・。