PHPで暗号学的に安全でランダムな文字列を柔軟に生成する方法


やりたいこと

  • ランダムな文字列を取得したいよ。
  • PHPの標準関数だけでやりたいよ。
  • 暗号学的に安全なランダム性を持たせたいよ。
  • 出力で使用する文字や文字長を指定したいよ。(0~fだけとかヤダよ)
  • PHP5系では動かせなくてもいいよ。

暗号学的に安全じゃない方法とは?

「ランダムな文字列をゲットしたいな」というとき、よく以下のようなコードが使われるかと思います。

echo sha1(uniqid(mt_rand(), true));
// 出力例 : f282fe197b5f9837a520118d5636facc0ff3832d

実はこれだと問題があって(まぁ実際はほぼ問題になることは無いと思いますが…)、「mt_rand()」も「uniqid()」も生成される値が暗号学的に安全ではないとリファレンスに明記されています。

PHPマニュアル - 関数リファレンス

あと、そもそもこの方法だと「出力で使用する文字を指定」したり、「出力文字長を指定」したりできないですね。

暗号学的に安全な方法1(0~f固定だけど簡単な方法)

出力される文字が0~f固定でも良い場合は、以下のコードでOKです。

$length = 16;
echo substr(bin2hex(random_bytes($length / 2 + 1)), 0, $length);
// 出力例 : cf7ffd47e6cb2399

random_bytes()は、暗号学的に安全なランダムバイト列を生成します。
生成したバイト列をbin2hex()で文字列に変換し、最後にsubstr()で必要な長さに切っています。

PHPマニュアル - 関数リファレンス

暗号学的に安全な方法2(0~f固定じゃないし文字長指定できるし使用文字を指定できる方法)

見た方が早いですね。以下の通りです。

function random_string($length = 16) {
    // 出力で使用する文字を決める。
    // 例えば、以下の場合は、「英数字から見間違え安い文字を除外した文字」のみにしてある。
    // (lとかOとか除外してある)
    $baseChars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789';
    $baseCharLength = strlen($baseChars);

    // 指定された数だけランダムな文字を取得する
    $output = '';
    for ($i = 0; $i < $length; $i++) {
        // rand()ではなく、暗号学的に安全なrandom_int()を使用する。
        // ちなみに、文字列は角括弧で任意の文字にアクセスできるので、substr()じゃなくてOK。
        $output .= $baseChars[random_int(0, $baseCharLength - 1)];
    }

    return $output;
}

コメントにも書いてありますが、出力で使用したい文字を$baseCharsで指定しています。
そこから、必要な文字長だけランダムに文字を取り出していく、という流れです。

このとき、ランダムな取り出し位置はrandom_int()で指定します。random_int()で生成される値は暗号学的に安全であることがリファレンスにも明記されています。

PHPマニュアル - 関数リファレンス

出力結果はこんな感じです。

for ($i = 0; $i < 5; $i++) {
    echo random_string(16)."\n";
}
// deWWGQ9wWGBVDFcU
// ui7usFxSx8FwWPCM
// d7AGQmydVcPbcwv6
// xzuVnAKCr5QB7Rz8
// SHHc8UV3Ha5F2th3

出力する文字にマルチバイト文字を使用したい場合

マルチバイト文字の場合は「strlenではなくmb_strlenを使う」、「文字列からの文字取り出しは角括弧ではなくmb_substrを使う」にします。

function mb_random_string_iroha($length = 16) {
    $baseChars = 'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせすん';
    // マルチバイト文字を使う場合は、mb_strlen()で。
    $baseCharLength = mb_strlen($baseChars);

    $output = '';
    for ($i = 0; $i < $length; $i++) {
        $pos = random_int(0, $baseCharLength - 1);
        // マルチバイト文字ならこちらもmb_substr()で。
        $output .= mb_substr($baseChars, $pos, 1);
    }

    return $output;
}

出力結果はこんな感じです。

for ($i = 0; $i < 5; $i++) {
    echo mb_random_string_iroha(16)."\n";
}
// なたれあなやろおおてほねさえれゆ
// あよらゑふめさたなりよゐむきたせ
// ゆてるいもそしやぬりゐてちひたか
// しのけちほねせねえつゆくすおめせ
// のつすいらなほれれむりとよまさる

最後に、高精度の姓名分割ツール とか Writeningっていうメモサービス とか、その他色々作ってるのでぜひプロフィール欄のホムペを覗いてみてください〜