random_bytesで安全なトークンを生成する
random_bytesとは、暗号論的に安全なランダムなバイト列を生成するPHPの関数です。トークンや鍵など推測されて困る値を作るときに使い、rand や uniqid の代わりになります。
rand や uniqid では足りない
トークンやセッションID、パスワードリセット用の鍵などを作るとき、rand や mt_rand、uniqid を使っているコードをたまに見かけます。これらは統計的にはランダムでも、暗号的には予測可能なので、セキュリティ用途には向きません。推測されると困る値を作るなら、暗号論的に安全な random_bytes を使うのが正しい選択だと思います。
// これらはセキュリティ用途に使わない
$weak = uniqid(); // 時刻ベースで予測されうる
$weak2 = mt_rand(); // 暗号的に安全ではない
// これが正解
$bytes = random_bytes(32); // 32バイトの安全なランダム
random_bytes は生のバイト列を返します。そのままだと表示や保存に使いにくいので、bin2hex で16進文字列に変換するのが定番ですね。
bin2hexで扱いやすい文字列に
32バイトを bin2hex に通すと64文字の16進文字列になります。URLやDBにそのまま載せられて扱いやすいですね。長さは用途に応じて決めますが、リセットトークンやAPIキーなら32バイト前後あれば総当たりは現実的に不可能な水準になります。短くしすぎると推測の余地が生まれるので、けちらないのがコツだと思います。
function generateToken(int $bytes = 32): string
{
return bin2hex(random_bytes($bytes));
}
$token = generateToken();
// 例: 3f7a...(64文字の16進文字列)
// URLに含めても安全
用途に応じた変換
URLに載せるなら16進よりも短くしたい、というときは base64url ふうに変換する手もあります。数字の範囲でランダムが欲しいなら random_int が対応するので、こちらも rand ではなく random_int を使うのが安全ですね。要は「予測されて困る値は必ず random_bytes か random_int」という一本の原則で統一しておくと迷いません。生成した値はハッシュ化して保存し、照合時は後述の hash_equals で比べる、という流れまで合わせておくと隙が少なくなる気がします。
// URLセーフな短めのトークン
$url = rtrim(strtr(base64_encode(random_bytes(24)), '+/', '-_'), '=');
// 数値範囲の安全な乱数
$code = random_int(100000, 999999); // 6桁の確認コード
まとめ
推測されて困る値を作るときは、rand や uniqid ではなく random_bytes、数値なら random_int、というのが原則だと思います。bin2hex で扱いやすい文字列にして、長さは十分に取る。生成した秘密は保存時にハッシュ化し、照合は定数時間で行う。ここまでを一続きの習慣にしておくと、トークン周りで足元をすくわれにくくなる気がします。
よくある質問
Q. 何バイト生成すればいいですか?
A. リセットトークンやAPIキーなら32バイトあれば総当たりは現実的に不可能な水準です。短くしすぎず、迷ったら32バイトにしておけば安心だと思います。
Q. random_bytesが例外を投げることはありますか?
A. 安全な乱数源が得られない環境では例外を投げます。まず起きませんが、生成を共通関数にまとめておくと対処が一箇所で済んで楽ですね。