土曜日, 6月 28, 2025
土曜日, 6月 28, 2025
- Advertisment -
ホームニューステックニュース 乱数は本当にランダムか? #Python - Qiita

[深層] 乱数は本当にランダムか? #Python – Qiita



[深層] 乱数は本当にランダムか? #Python - Qiita

image.png

金曜日の夕方、デプロイも終わってほっと一息ついた開発チームの雑談タイムでのことでした。

「ところで、Math.random()って本当にランダムなんですか?」

50代後半のベテランエンジニア、滝沢さんが苦笑いを浮かべました。「またこの話題か。これ、うちの会社では2年に1度は必ず誰かが言い出すんだよね」

同じく50代の愛田さんも懐かしそうに頷きます。興味深くその会話を聞いていました。

そう、この「乱数は本当にランダムか?」という問いは、エンジニアにとって避けて通れない哲学的かつ実践的なテーマなのです。今回は、この永遠の問いに改めて向き合ってみましょう。

そもそも「ランダム」とは何か

予測不可能性という幻想

まず、私たちが日常的に使っている「ランダム」という言葉の意味を考えてみましょう。

# よくあるコード
import random
random_value = random.random()
print(random_value)  # 0.7394508971214333

このコードを実行するたびに異なる値が出力されます。一見すると完全にランダムに見えますが、実はこれは「疑似乱数」と呼ばれるものです。

なぜ「疑似」なのでしょうか。それは、コンピュータが生成する乱数は、必ず何らかのアルゴリズムに基づいているからです。つまり、理論的には予測可能なのです。

決定論的カオスの世界

現代のプログラミング言語で使われる疑似乱数生成器の多くは、「線形合同法1や「メルセンヌ・ツイスタ2といったアルゴリズムを使用しています。

# 線形合同法の簡単な実装例
class SimpleRandom:
    def __init__(self, seed=1):
        self.seed = seed
    
    def next(self):
        # X(n+1) = (a * X(n) + c) mod m
        self.seed = (1103515245 * self.seed + 12345) % (2**31)
        return self.seed / (2**31)

# 同じシードからは同じ系列が生成される
rng = SimpleRandom(42)
print([rng.next() for _ in range(5)])
# 常に同じ結果: [0.3707, 0.2641, 0.5435, 0.0016, 0.8981]

このコードが示すように、同じ初期値(シード)から始めれば、常に同じ数列が生成されます。これが疑似乱数の本質です。

実は深刻な問題「乱数の品質」

PlayStation 3 の暗号鍵流出事件

疑似乱数の予測可能性は、時として深刻なセキュリティ問題を引き起こします。

2010年、PlayStation 3のセキュリティが破られた事件を覚えているでしょうか。この事件の原因の一つは、ソニーがECDSA3署名に使用する乱数(nonce4)として、実際には固定値を使用していたことでした(参考文献1)。

# ECDSA署名における脆弱な実装の概念図
def vulnerable_ecdsa_sign(message, private_key, curve):
    # 本来は毎回異なるkを生成すべき
    k = 42  # nonce再利用の脆弱性!
    
    # 楕円曲線上の点の計算
    R = curve.scalar_mult(k, curve.G)
    r = R.x % curve.n
    
    # 署名の計算
    z = hash(message)
    s = (pow(k, -1, curve.n) * (z + private_key * r)) % curve.n
    
    return (r, s)

このような実装では、複数の署名から秘密鍵を逆算することが可能になってしまいます。

「それで思い出したんだけど」と口を開きました。「以前、本番環境で疑似乱数のシードを固定したまま3ヶ月気づかなかったことがあってね…」

愛田さんが深くうなずきます。「あー、あるある。俺も昔、乱数で生成したはずのユーザーIDが妙に偏ってるって指摘されて、調べたらテスト用のシード値が残ってたことがあったよ」

Debian OpenSSLの惨事

2008年、Debian LinuxのOpenSSLパッケージに重大な脆弱性が発見されました(参考文献2)。メンテナが「不要」と判断してコメントアウトしたコードが、実は乱数生成のエントロピー5源だったのです。

// 問題のあったコード(概念図)
// MD_Update(&m, buf, j);  /* このコメントアウトが大惨事に */
MD_Update(&m, &dummy, 1);  /* 実質的にエントロピーゼロ */

この結果、生成可能な鍵の種類が激減し、総当たり攻撃が現実的になってしまいました。

エンジニアの永遠の悩み「どの乱数を使うべきか」

身近な例、オンラインゲームのダイスロール

「そういえば、去年うちのゲームチームが面白いクレームを受けたんだ」と思い出したように言いました。「『このゲームのサイコロ、絶対イカサマしてる!6が出る確率が低すぎる!』って」

元橋さんが興味深そうに聞きます。「実際に偏りがあったんですか?」

「いや、完全に正常だった。でも人間の認知バイアスってやつでね」愛田さんが説明を引き継ぎました。「本当にランダムな結果って、人間には偏って見えることがあるんだ」

# よくあるダイス実装
import random

def roll_dice():
    return random.randint(1, 6)

# 1万回振った結果を集計
results = {}
for _ in range(10000):
    roll = roll_dice()
    results[roll] = results.get(roll, 0) + 1

print(results)
# {1: 1667, 2: 1654, 3: 1672, 4: 1668, 5: 1665, 6: 1674}
# ほぼ均等だが、人は連続して同じ目が出ると「おかしい」と感じる

「実はもっと深刻な問題もあってね」滝沢さんが続けました。「昔、あるオンラインカジノで、疑似乱数の周期が短すぎて、プレイヤーにパターンを読まれた事件があった」

# 危険な実装例、予測可能なダイス
class PoorDice:
    def __init__(self):
        self.state = 12345  # 固定初期値
    
    def roll(self):
        # 単純すぎる線形合同法
        self.state = (self.state * 1103515245 + 12345) & 0xFFFF
        return (self.state % 6) + 1

# 同じパターンが繰り返される
dice = PoorDice()
pattern = [dice.roll() for _ in range(20)]
print(pattern)  # 毎回同じ: [4, 4, 5, 1, 1, 2, 3, 3, 4, 5, 5, 6, 2, 2, 3, 4, 4, 5, 1, 1]

物理サイコロ vs デジタルダイス

「でも物理的なサイコロだって完全にランダムじゃないですよね」元橋さんが鋭い指摘をしました。

「その通り!」と興奮気味に答えました。「カジノでは精密に作られたサイコロを使うけど、それでも重心の偏りや投げ方で結果に影響が出る。だから定期的に交換してるんだ」

用途別乱数選択ガイド

実際の開発では、用途に応じて適切な乱数生成方法を選ぶ必要があります。

// ゲームやシミュレーション用
const gameRandom = Math.random();

// セキュリティが重要な場合
const crypto = require('crypto');
const secureRandom = crypto.randomBytes(32);

// より高品質な疑似乱数が必要な場合(Node.js)
const { randomInt } = require('crypto');
const highQualityRandom = randomInt(0, 100);

しかし、どんなに優れた疑似乱数生成器を使っても、それは所詮「疑似」でしかありません。

真のランダムを求めて「熱雑音」という希望

物理現象を利用した真の乱数

「実は、真のランダムを生成する方法はあるんだよ」と切り出しました。

ここで登場するのが「真性乱数生成器(TRNG6」です。これは、予測不可能な物理現象を利用して乱数を生成します。

最も一般的なのが「熱雑音」を利用した方法です。抵抗器に流れる電流には、原子の熱運動による微小なゆらぎ(ジョンソン・ナイキスト雑音7)が含まれています。

「おお、物理屋の血が騒ぐ話だ」愛田さんが身を乗り出しました。「確か絶対零度でない限り、必ず熱雑音は発生するんだよな」

熱雑音の原理
├─ 原子の熱運動
│  ├─ 統計的には予測可能
│  └─ 個々の粒子レベルでは予測不可能
└─ 電気信号への変換
   ├─ アナログ信号の測定
   └─ デジタル値への変換

現実世界での実装

IntelのCPUには「RDRAND」命令が実装されており、チップ内の熱雑音を利用した真性乱数を生成できます(参考文献3)。ただし、2013年にNSAバックドア疑惑が浮上し、Linuxカーネルは予防措置としてRDRAND出力を直接使用せず、内部エントロピープールにXOR混入する設計を採用しています(参考文献4)。

# Linux環境でのハードウェア乱数取得
import os

def get_hardware_random(num_bytes=32):
    """
    /dev/urandomは最新のLinuxカーネルでは
    必要に応じてハードウェア乱数を使用
    """
    return os.urandom(num_bytes)

# より直接的にハードウェア乱数を使う場合
def get_true_random(num_bytes=32):
    """
    /dev/random はLinux 5.6以降では十分にシードされた後は
    ブロックしなくなったが、ブート直後などエントロピープールが
    初期化されていない状態では依然としてブロックする
    """
    with open('/dev/random', 'rb') as f:
        return f.read(num_bytes)

また、専用のハードウェア乱数生成器も存在します。これらは放射性崩壊、光量子、大気雑音などを利用して、真のランダム性を実現しています。

「うちもセキュリティ監査で指摘されて、去年HSM8(Hardware Security Module)導入したよ」滝沢さんが実体験を語ります。「月額料金見て腰抜かしたけど、セキュリティには代えられないからね」

クラウド時代の乱数生成

「そういえば、AWS KMSの乱数生成機能使ったことある?」と話題を振りました。

「あるある!」愛田さんが反応しました。「CloudFormationでランダムなパスワード生成するときに使ったよ」

import boto3

# AWS KMSを使った暗号学的に安全な乱数生成
kms_client = boto3.client('kms', region_name='ap-northeast-1')

def generate_secure_random_aws(num_bytes=32):
    response = kms_client.generate_random(NumberOfBytes=num_bytes)
    return response['Plaintext']

# Secrets Managerでのランダムパスワード生成
sm_client = boto3.client('secretsmanager', region_name='ap-northeast-1')

def create_random_password():
    response = sm_client.get_random_password(
        PasswordLength=32,
        ExcludeCharacters=' %+~`#()|[]{}:;?!\'/@"\\',
        ExcludePunctuation=False,
        RequireEachIncludedType=True
    )
    return response['RandomPassword']

「Cloudflareも面白いですよ」元橋さんが意外な知識を披露しました。「Lava Lampを使った乱数生成って知ってますか?」

「ああ、あのカラフルなやつか!」滝沢さんが笑いました。「Cloudflareのサンフランシスコオフィスで、ラバランプの動きをカメラで撮影して、その映像から乱数を生成してるんだよな」

# Cloudflare API経由でのランダムデータ取得(概念的な例)
import requests

def get_cloudflare_random():
    """
    実際のCloudflare APIとは異なりますが、
    彼らのエントロピー源は物理現象ベース
    """
    # Cloudflareは実際にはdrand(分散型乱数ビーコン)なども提供
    response = requests.get('https://drand.cloudflare.com/public/latest')
    return response.json()['randomness']

「物理現象を使うっていう意味では、熱雑音もラバランプも同じ発想だよね」と総括しました。「予測不可能な自然現象をデジタル化する」

なぜこの話題は2年に1度盛り上がるのか

エンジニアの性(さが)

経験上、この「乱数は本当にランダムか?」という話題は、確かに定期的に社内で盛り上がります。その理由を考えてみました。

「面白いことに、だいたい2年周期なんだよね」と滝沢さんが振り返ります。「前回は2年前の夏、その前は2019年の忘年会だったかな」

1. 新人エンジニアの素朴な疑問

技術に真摯に向き合う新人ほど、この本質的な問いを投げかけてきます。そして、それをきっかけにベテランエンジニアたちの議論が始まるのです。

「元橋くんみたいに素直に疑問を持つのは大事だよ」と愛田さんが元橋さんに声をかけました。「俺も30年前、同じ質問して先輩に3時間説教されたからね」

2. 実装での失敗体験

# よくある失敗例
import random

# テストで固定シードを使ったまま本番にデプロイ
random.seed(42)  # これを削除し忘れる

# 結果、「ランダム」なはずの処理が毎回同じ結果に...

このような失敗を経験したエンジニアは、乱数の本質について深く考えるようになります。

3. セキュリティインシデントのニュース

定期的に発生する乱数関連のセキュリティ事件が、この話題を再燃させます。

哲学的な魅力

さらに、この話題には哲学的な魅力があります。

「完全にランダムなものは存在するのか?」
「決定論的な宇宙で真のランダムは可能なのか?」
「量子力学的な不確定性は本当にランダムなのか?」

「ラプラスの悪魔って知ってる?」愛田さんが哲学モードに入りました。「宇宙のすべての原子の位置と運動量がわかれば、未来は完全に予測できるって話。もしそれが本当なら、真のランダムなんて存在しないことになる」

「でも量子力学では…」と元橋さんが反論しようとすると、滝沢さんがニヤリと笑いました。「お、2年目のくせに量子力学まで持ち出すか。議論がさらに2時間延長だな」

これらの問いは、エンジニアリングの枠を超えて、私たちの世界観にまで踏み込んできます。

実践的なアドバイス

今すぐチェックすべきこと

あなたのプロジェクトで以下のような実装をしていないか、確認してみてください。

# NGパターン、セキュリティに関わる処理でrandomモジュールを使用
import random
import string

chars = string.ascii_letters + string.digits
session_id = ''.join(random.choices(chars, k=16))

# OKパターン、暗号学的に安全な乱数を使用
import secrets
session_id = secrets.token_hex(16)
# NGパターン、パスワード生成に通常の乱数を使用
import random
password = ''.join(random.choices(chars, k=16))

# OKパターン、secrets モジュールを使用
import secrets
password = ''.join(secrets.choice(chars) for _ in range(16))

用途に応じた使い分け

  1. ゲーム・シミュレーション → 疑似乱数で十分
  2. 統計的サンプリング → 高品質な疑似乱数(メルセンヌ・ツイスタ等)
  3. 暗号・セキュリティ → 暗号学的に安全な乱数9(/dev/urandom、CryptoAPI等)
  4. 超高セキュリティ → ハードウェア乱数生成器の検討

おわりに「不確実性を受け入れる勇気」

「乱数は本当にランダムか?」

この問いに対する答えは、「何をもってランダムとするか」によって変わります。疑似乱数は決定論的ですが、実用上は十分にランダムです。真性乱数は物理法則に基づきますが、その物理法則自体が確率的です。

「結局のところ」愛田さんがぽつりと呟きました。「世界は灰色の光に包まれているんだよ。完全な白でも黒でもない。決定論と非決定論の狭間で、俺たちはコードを書いている」

その言葉に、一同は静かに頷きました。

重要なのは、この不確実性を理解し、適切に扱うことです。完璧なランダムを追求するのではなく、用途に応じた「十分に良い」ランダムを選択する。これこそが、エンジニアとしての実践的な知恵なのかもしれません。

次回、あなたの職場でこの話題が持ち上がったとき、あなたはどんな視点で議論に参加しますか?そして、その議論の中で、新たな発見があることを願っています。

きっと2年後、また誰かが同じ質問をするでしょう。そのとき、この記事が議論の出発点になれば幸いです。

「じゃあ、そろそろ帰るか」滝沢さんが伸びをしながら立ち上がりました。「元橋くん、良い質問だったよ。おかげで久しぶりに熱い議論ができた」

時計を見て驚きました。もう20時を過ぎています。乱数談義に花が咲いて、すっかり時間を忘れていたようです。


参考文献

  1. fail0verflow. (2010). Console Hacking 2010: PS3 Epic Fail. 27th Chaos Communication Congress (27C3). Retrieved from https://events.ccc.de/congress/2010/Fahrplan/events/4087.en.html

  2. Debian Security Team. (2008). DSA-1571-1 openssl — predictable random number generator. Debian Security Advisory. Retrieved from https://www.debian.org/security/2008/dsa-1571

  3. Intel Corporation. (2018). Intel Digital Random Number Generator (DRNG) Software Implementation Guide. Retrieved from https://www.intel.com/content/www/us/en/developer/articles/guide/intel-digital-random-number-generator-drng-software-implementation-guide.html





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -