疑似乱数(PRNG)と暗号学的乱数(CSPRNG):どちらをいつ使うか

約5分

「乱数」と一口に言っても、ゲームでサイコロを振るのと暗号鍵を生成するのでは要求される品質がまるで違います。本記事では PRNG(疑似乱数)と CSPRNG(暗号学的疑似乱数)の違いと、用途別の選び方を整理します。

真の乱数 vs 疑似乱数

真の乱数(TRNG)

物理現象から生成される乱数:

  • 熱雑音:CPU の熱ノイズなどから取得
  • 量子現象:放射性崩壊、光子検出
  • 環境ノイズ:マウスの動き、ディスク I/O のタイミング

純粋にランダム。OS の /dev/random(Linux)はエントロピープールから取得します。

疑似乱数(PRNG)

数学的アルゴリズムで「乱数っぽい」数列を生成:

  • 同じシード(初期値)を与えれば同じ列が出る → 再現可能
  • 速い、軽量
  • 純粋にはランダムではない

ゲームのサイコロ、シミュレーション、ランダムサンプリング、A/B テストの割り当てなどで使われます。

CSPRNG:暗号学的に安全な PRNG

PRNG の中でも特に強い性質を満たすもの:

  • 出力の一部から将来の出力を予測できない
  • 内部状態が漏洩しても過去の出力を復元できない(Forward Secrecy)

通常の PRNG とは設計が違い、ChaCha20、AES-CTR DRBG などの暗号アルゴリズムをベースにしています。

Math.random() の限界

JavaScript の Math.random()PRNGであって CSPRNG ではありません:

  • 内部実装はブラウザによる(多くは xorshift128+ など)
  • セキュリティ用途には不適切
  • パスワード、トークン、暗号鍵には絶対に使わない
Math.random(); // PRNG、シミュレーション・ゲーム用途

CSPRNG の API

ブラウザ

const arr = new Uint8Array(16);
crypto.getRandomValues(arr); // CSPRNG

または UUID 生成:

crypto.randomUUID(); // CSPRNG ベースの v4 UUID

Node.js

import { randomBytes, randomInt, randomUUID } from 'node:crypto';
randomBytes(32); // 32 バイトのランダム
randomInt(0, 100); // 0-99 の整数
randomUUID(); // v4 UUID

Python

import secrets
secrets.token_bytes(32)    # CSPRNG
secrets.token_urlsafe(32)  # URL safe な文字列
secrets.randbelow(100)     # 0-99 の整数

random モジュールは PRNG なのでセキュリティ用途は secrets を使う。

シードと再現性

PRNG はシードを与えると同じ列が出ます。これは:

  • テスト:同じ入力でテストを再現可能にする
  • シミュレーション:研究の再現性を確保
  • ゲーム:ランダム生成されたマップを再現する(Minecraft のシード)
// 自前 PRNG の例(mulberry32)
function mulberry32(seed) {
	return () => {
		let t = (seed += 0x6d2b79f5);
		t = Math.imul(t ^ (t >>> 15), t | 1);
		t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
		return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
	};
}
const rand = mulberry32(42);
rand(); // 同じシードなら毎回同じ値

Math.random() はシード指定不可なので、再現性が必要なら自前 PRNG を使います。

用途別の選び方

用途推奨
ゲームのサイコロ、エフェクトMath.random()
シミュレーション(再現性必要)シード可能な PRNG
A/B テストの割り当てハッシュベース(user_id を SHA-256 で)
パスワード生成CSPRNG
暗号鍵、トークンCSPRNG
UUID v4crypto.randomUUID()
セッション IDCSPRNG(256bit 以上)
抽選・くじ場合による(厳密性が要れば CSPRNG)

偏りの罠:% N は均等じゃない

「0〜99 のランダム整数」を作るのに Math.random() * 100 を使うのは OK ですが、整数 PRNG の出力に % N をすると偏りが出ます:

// 32 bit ランダム → 100 で割る
const r = randomInt32() % 100; // ❌ わずかに偏る

理由:2^32 は 100 で割り切れない。0〜95 が出る確率がわずかに高くなります。

正しい方法は 棄却サンプリング

function unbiased100() {
	const max = Math.floor(2 ** 32 / 100) * 100;
	while (true) {
		const r = randomInt32();
		if (r < max) return r % 100;
	}
}

JavaScript の crypto.randomInt(0, 100) などは内部でこれを行ってくれます。

まとめ

  • PRNG:速い、シードで再現可能、セキュリティ用途には不可
  • CSPRNG:暗号学的に安全、パスワード・鍵・トークンに必須
  • Math.random() は PRNG、セキュリティには crypto.getRandomValues()
  • % N は偏りの原因、棄却サンプリングか専用 API を使う

範囲指定で乱数を生成したいときは、本サイトの乱数生成ツールが使えます。範囲・個数・重複可否を指定できます。