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. 安全な乱数源が得られない環境では例外を投げます。まず起きませんが、生成を共通関数にまとめておくと対処が一箇所で済んで楽ですね。

類似投稿

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です