PHPでパスワード用にランダムな文字列を生成してみた話

# はじめに
[ユアマイスターアドベントカレンダー2018](https://qiita.com/advent-calendar/2018/yourmystar) の15日目の記事です。
現在絶賛卒業研究中の大学インターンのmasaki-ogawaです。

今回は普段使っているPHPでランダムな文字列を生成してみるという内容です。ちょっとした暗号のアルゴリズムにも触れることができました。

## きっかけ
最近、色々なサイトに会員登録するということをやっています。
**(後ろめたいことをしているわけではありません)**
卒業研究の情報リサーチのために、メディアサイトに会員登録をしたりと本当に色々登録しています。**最近約20個近くのサイトに新規会員登録しました。**
そこで浮き彫りになったことは、ユーザーIDやパスワードをどうしようかという問題でした。

## パスワードについて
自分は今まで心に決めたユーザーIDやパスワードは両手で数えられる程度しか持っていませんでした。
ただ、複数のサイトに会員登録するにあたって、ユーザーIDやパスワード一緒で大丈夫なのか？という疑問が生まれました。理由としては、何かのきっかけで、流出してしまった場合、自分が会員登録をしているサイトにログインできてしまうんではないかという不安がありました。規模が小さいサイトに情報を登録することもあったので、セキュリティ大丈夫か？と思ったり、Amazonとかにはクレジットカードが登録してあるので、不正利用とか大丈夫かなという不安も生まれました。

## 今回やったこと
ちょっとしたスクリプトを書いて、ユーザーIDやパスワード用に文字列をランダム生成できれば、サイトごとにユーザーIDやパスワードが変えられるのではと思い今回ちょっとしたものを書いてみることにしました。

### array_randを使って配列からランダムに文字を取得する
まず頭に浮かんだのは、任意の文字列を格納した配列から、`array_rand`関数を使用して、ランダムkeyうを取得して文字列をとってみるということでした。
ちなみに、`range('a', 'z')`で小文字の`a`~`z`の配列を取得しており、`A`~`Z`、`0`~`9`を使用する文字として今回は行います。
今回は試しに5桁の文字列を生成します。

```php:index.php
// 使用する文字列を配列に格納
$characters = array_merge(range('a', 'z'), range('A', 'Z'), range('0', '9'));

while (true) {
    // ランダムにkeyを取得
    $key = array_rand($characters);
    $character =  $characters[$key];

    $password[] = $character;

    if (count($password) === 5) {
        // 配列を文字列に変換
        $password = implode($password);
    }
}
```

といった感じでランダムな文字列が作成できました。ただ、一方でこのコードだけで、セキュリティ性がある文字列が作成できるのかというと微妙です。
[array_rand](http://php.net/manual/ja/function.array-rand.php)のドキュメントでは
`一つ以上のランダムなエントリを配列から取り出し、 取り出したエントリのキーを返します。 この関数が使う疑似乱数生成器は、暗号学的な使い方には適していません。`
**暗号学的な使い方には適していません。**としっかり書いてありました。この案はダメそうです。

### 暗号論的擬似乱数生成器とは
暗号論的擬似乱数生成器を知らなかったので、少し調査してみました。
>暗号論的擬似乱数生成器（英語: cryptographically secure pseudo random number generator、暗号論的にセキュアな疑似乱数生成器、CSPRNG）とは、暗号技術での利用に適した特性を持つ擬似乱数生成器 (PRNG) である。その際に必要な乱数の性質は様々である。例えば、何らかの暗号プロトコルで Nonce を生成する際に求められるのは一意性だけである。一方、鍵の生成には高い無作為性が求められる。ワンタイムパッドには暗号論的擬似乱数も不適で、高いエントロピーを持つ真の無作為情報源が必要であり、それにより情報理論的安全性を得る。

[暗号論的擬似乱数生成器](https://ja.wikipedia.org/wiki/%E6%9A%97%E5%8F%B7%E8%AB%96%E7%9A%84%E6%93%AC%E4%BC%BC%E4%B9%B1%E6%95%B0%E7%94%9F%E6%88%90%E5%99%A8)wikipediaより

### 情報理論的安全性とは
>暗号理論において、情報理論的安全性（じょうほうりろんてきあんぜんせい）とは、暗号に対する攻撃（暗号解読）に対する強度（安全性）に関する概念の一つであり、一般に計算量的安全性よりも強い。この安全性を満たす暗号では「どんな鍵によって得られるどんな復号結果も、同様に確からしい」ので、どれほどの計算力をもってしても、解読は不可能である。

[情報理論的安全性](https://ja.wikipedia.org/wiki/%E6%83%85%E5%A0%B1%E7%90%86%E8%AB%96%E7%9A%84%E5%AE%89%E5%85%A8%E6%80%A7)wikipediaより

つまり、解読不可能な乱数を生成できるというものが**暗号論的擬似乱数生成器**ということです。


## PHP7から加わったrandom_bytes関数を使用する
PHP7系以前は、ユーザー定義で暗号論的擬似乱数生成器に順ずる、ランダムな文字列を生成していましたが、PHP7系からは`random_bytes`という関数が追加されました。なので、これを使用しようと思います。

```php:index.php
substr(bin2hex(random_bytes($length)), 0, 5);
```
この一行で、暗号理論的にもお墨付きな文字列が生成できました。
細かい説明をしておくと、
[random_bytes](http://php.net/manual/ja/function.array-rand.php) 暗号理論的に安全なランダムバイト生成 0~9,a~fで構成される
[bin2hex](http://php.net/manual/ja/function.bin2hex.php) バイナリのデータを16進表現に変換する
[substr](http://php.net/manual/ja/function.substr.php) 文字列の一部を返す
ということで今回でいえば、ランダムバイトを16進数に変換したのち、5文字だけ取り出すということを行いまいした。

# まとめ
結論から言えば、今回は１番良いのは、`random_bytes`を使うことだと思います。ただし、0~9,a~fの範囲のみなので、文字の重複が起こりやすいということがあります。
こちらの[ランダムなパスワードを1行で生成する](https://qiita.com/suin/items/c958bcca90262467f2c0)が参考になりました。
ケースに応じて、実装を使い分けると良いと思います。
自分が思いつく以外にも色々な方法があるんだなと思いました。
PHP以外の言語にも、random_bytesにあたる関数が用意されているのかも今後調査してみたいと思います。

# 最後に
現在、エンジニア用Twitterアカウント[@gawa2101](https://twitter.com/gawa2101)を開設しました。
今後、自身のアウトプットを積極的にしていこうと考えているのでフォローしていただけると幸いです。