12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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時を過ぎています。乱数談義に花が咲いて、すっかり時間を忘れていたようです。

乱数生成に関する実験が行えるGalton Boardを作成いたしました。さまざまな乱数生成方法を試すことが可能で、簡単に可視化して偏りを確認できます。
https://masato-shigemori.github.io/galton-board/

参考文献

  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

  1. 線形合同法(Linear Congruential Generator, LCG) - 最も基本的な疑似乱数生成アルゴリズム。X(n+1) = (a × X(n) + c) mod m という式で次の乱数を生成する。高速だが周期が短く品質は高くない。

  2. メルセンヌ・ツイスタ(Mersenne Twister) - 松本眞と西村拓士が1997年に開発した疑似乱数生成器。周期が2^19937-1と非常に長く、統計的性質も優れている。多くのプログラミング言語の標準乱数生成器として採用されている。

  3. ECDSA(Elliptic Curve Digital Signature Algorithm) - 楕円曲線暗号を使用したデジタル署名アルゴリズム。RSAよりも短い鍵長で同等のセキュリティを実現できる。

  4. nonce(Number used once) - 暗号プロトコルで一度だけ使用される数値。同じnonceを再利用すると、暗号が破られる原因となる。

  5. エントロピー(Entropy) - 情報理論では「不確実性の度合い」を表す。乱数生成においては、予測不可能性の源となるランダムなデータのこと。

  6. TRNG(True Random Number Generator) - 真性乱数生成器。熱雑音、放射性崩壊、光量子など、本質的に予測不可能な物理現象を利用して乱数を生成する装置。

  7. ジョンソン・ナイキスト雑音(Johnson-Nyquist noise) - 抵抗器内の電子の熱運動によって生じる電気的雑音。温度に比例し、絶対零度でない限り必ず発生する。

  8. HSM(Hardware Security Module) - 暗号鍵の生成・保管・処理を専門に行うハードウェアデバイス。物理的な耐タンパー性を持ち、真性乱数生成器を内蔵していることが多い。

  9. 暗号学的に安全な乱数(CSPRNG: Cryptographically Secure Pseudo Random Number Generator) - 暗号用途に使用できる品質の疑似乱数生成器。過去の出力から将来の出力を予測することが計算量的に困難である必要がある。

12
9
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?