隨機數:加密系統中最被低估的組件
1. 為什麼要關心這個問題?
2010 年,PlayStation 3 的安全系統被完全破解。不是因為加密演算法有問題,而是因為 Sony 在數位簽章時用了一個固定的「隨機」數。
# Sony 的致命錯誤(簡化版)
def sign_game(game_data):
k = 4 # 這個數字應該每次都不同!
signature = ecdsa_sign(game_data, private_key, k)
return signature結果?任何人都可以計算出 Sony 的私鑰,為任意程式碼簽章,在 PS3 上執行盜版遊戲和自製軟體。
一個「隨機數」不隨機,整個安全系統崩潰。
這不是個案。從 Debian OpenSSL 漏洞(2008)到 Android Bitcoin 錢包被盜(2013),歷史上無數安全災難都源於隨機數問題。
2. 定義
隨機數在密碼學中指的是不可預測的數值,用於產生金鑰、初始化向量(IV)、nonce、鹽值等。
密碼學安全的隨機數必須具備:
- 不可預測性: 即使知道之前產生的所有數字,也無法預測下一個
- 不可重現性: 相同的條件不會產生相同的序列
- 均勻分佈: 所有可能的值出現機率相等
關鍵區別:
- 真隨機數(TRNG): 來自物理現象,真正不可預測
- 偽隨機數(PRNG): 演算法產生,看起來隨機但實際上是確定性的
- 密碼學安全偽隨機數(CSPRNG): 特殊設計的 PRNG,即使知道部分輸出也無法預測其他輸出
3. 為什麼隨機數這麼重要
金鑰產生
金鑰 = random(256 bits)
如果 random() 可預測:
- 攻擊者可以猜測金鑰
- 加密形同虛設一個 256 位元的 AES 金鑰有 2^256 種可能。但如果你的隨機數產生器只能產生 2^32 種不同的金鑰(因為種子只有 32 位元),攻擊者只需要嘗試 42 億次——幾小時就能完成。
初始化向量(IV)
AES-CBC 加密:
ciphertext = AES_CBC(plaintext, key, IV)
如果 IV 可預測:
- 攻擊者可以進行選擇明文攻擊
- 即使金鑰安全,加密仍可能被破解數位簽章中的 Nonce
這就是 Sony PS3 被破解的原因:
ECDSA 簽章:
signature = sign(message, private_key, k)
如果 k 重複使用:
private_key = (message1 - message2) / (signature1 - signature2)
↑ 直接計算出私鑰!4. rand() 為什麼會害死人
標準函式庫的 rand() 不是為安全設計的
// C 語言的 rand() - 絕對不要用於密碼學!
int seed = time(NULL); // 種子是當前時間
srand(seed);
int key = rand(); // 「隨機」金鑰問題:
- 種子太小: 通常只有 32 位元,最多 42 億種可能
- 種子可預測:
time(NULL)是當前秒數,攻擊者大概知道你何時產生金鑰 - 演算法簡單: 線性同餘產生器(LCG),數學上容易逆推
# Python 的 random 模組 - 同樣不安全!
import random
random.seed() # 使用系統時間
key = random.getrandbits(256) # 不要這樣做!真實案例:Debian OpenSSL 災難(2008)
// 有人「修復」了這段程式碼中的警告
MD_Update(&m, buf, j); // 被刪除
^
|
Valgrind 說這個變數未初始化
MD_Update(&m, &(md_c[0]), sizeof(md_c)); // 保留這個「修復」移除了熵的主要來源。結果:
- OpenSSL 只能產生 32,768 種不同的金鑰
- 所有在受影響系統上產生的 SSH 和 SSL 憑證都可以被暴力破解
- 影響了數百萬台 Debian/Ubuntu 伺服器
5. 真隨機 vs 偽隨機
真隨機數(TRNG)
來源:物理現象
- 放射性衰變
- 熱雜訊
- 量子效應
- 滑鼠移動、鍵盤時序
物理現象 → 測量 → 數位化 → 真隨機位元優點:真正不可預測 缺點:慢、需要特殊硬體、位元率有限
偽隨機數(PRNG)
種子 → 演算法 → 看起來隨機的序列# 簡化的 PRNG 演算法
def prng(seed):
state = seed
while True:
state = (state * 1103515245 + 12345) % 2**31
yield state優點:快、可重現(對測試有用) 缺點:完全確定性——知道演算法和種子就知道全部輸出
密碼學安全偽隨機數(CSPRNG)
專門為安全設計的 PRNG:
熵池 ──► CSPRNG ──► 安全的隨機輸出
↑
持續收集熵特性:
- 即使看到部分輸出,也無法預測其他輸出
- 即使內部狀態被洩漏,也無法恢復之前的輸出
- 持續從環境收集熵來更新內部狀態
6. 作業系統的隨機性來源
Linux: /dev/random vs /dev/urandom
# 阻塞式 - 熵不足時會等待
head -c 32 /dev/random
# 非阻塞式 - 總是立即返回
head -c 32 /dev/urandom現代建議:幾乎總是使用 /dev/urandom
歷史上的擔憂(已過時):
/dev/random「更安全」因為會等待真熵/dev/urandom可能「熵不足」
現實:
- 現代 Linux 的 urandom 使用相同的熵池
- 系統啟動後,urandom 在密碼學上與 random 一樣安全
/dev/random的阻塞只會造成問題(DoS、效能)
熵的來源
┌─────────────────────────────────────────────────────────────┐
│ 硬體熵來源 │
├─────────────────────────────────────────────────────────────┤
│ - CPU 時序抖動 │
│ - 硬體隨機數產生器(RDRAND/RDSEED 指令) │
│ - TPM(可信平台模組) │
│ - 專用熵產生硬體 │
├─────────────────────────────────────────────────────────────┤
│ 軟體熵來源 │
├─────────────────────────────────────────────────────────────┤
│ - 中斷時序 │
│ - 磁碟 I/O 時序 │
│ - 網路封包時序 │
│ - 鍵盤/滑鼠事件 │
└─────────────────────────────────────────────────────────────┘Windows: CryptGenRandom / BCryptGenRandom
// Windows API
BYTE buffer[32];
BCryptGenRandom(NULL, buffer, 32, BCRYPT_USE_SYSTEM_PREFERRED_RNG);macOS/iOS: SecRandomCopyBytes
var bytes = [UInt8](repeating: 0, count: 32)
let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes)7. 各語言的正確用法
Python
import secrets # Python 3.6+,專為密碼學設計
# 產生安全的隨機位元組
key = secrets.token_bytes(32) # 256 位元金鑰
# 產生安全的十六進位字串
token = secrets.token_hex(32) # 64 字元的 hex 字串
# 產生 URL 安全的 token
url_token = secrets.token_urlsafe(32)
# 在範圍內選擇安全的隨機數
dice = secrets.randbelow(6) + 1 # 1-6
# 安全的隨機選擇
password_char = secrets.choice('abcdefghijklmnopqrstuvwxyz0123456789')# 絕對不要這樣做!
import random
key = random.getrandbits(256) # 不安全!Node.js
const crypto = require('crypto');
// 產生安全的隨機位元組
const key = crypto.randomBytes(32);
// 產生安全的 UUID
const { randomUUID } = require('crypto');
const uuid = randomUUID();
// 產生範圍內的安全隨機數
function secureRandomInt(min, max) {
const range = max - min;
const bytesNeeded = Math.ceil(Math.log2(range) / 8);
const randomBytes = crypto.randomBytes(bytesNeeded);
const randomValue = parseInt(randomBytes.toString('hex'), 16);
return min + (randomValue % range);
}// 絕對不要這樣做!
const key = Math.random() * 2**256; // 不安全!Go
import (
"crypto/rand"
"encoding/hex"
)
// 產生安全的隨機位元組
func generateKey() ([]byte, error) {
key := make([]byte, 32)
_, err := rand.Read(key)
if err != nil {
return nil, err
}
return key, nil
}
// 產生安全的十六進位字串
func generateToken() (string, error) {
bytes := make([]byte, 32)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return hex.EncodeToString(bytes), nil
}// 絕對不要這樣做!
import "math/rand"
key := rand.Int63() // 不安全!Rust
use rand::Rng;
use rand::rngs::OsRng;
fn main() {
// 使用 OS 的 CSPRNG
let mut key = [0u8; 32];
OsRng.fill(&mut key);
// 產生範圍內的安全隨機數
let n: u32 = OsRng.gen_range(1..=100);
}8. 常見錯誤與防範
錯誤 1:使用時間作為種子
# 錯誤
import random
random.seed(int(time.time()))
key = random.getrandbits(256)
# 攻擊者知道你大概何時產生金鑰
# 只需要嘗試那段時間內的所有秒數錯誤 2:重複使用 nonce/IV
# 錯誤
iv = b'0' * 16 # 固定 IV
for message in messages:
ciphertext = aes_cbc_encrypt(message, key, iv) # 相同 IV!錯誤 3:縮小隨機數範圍
# 錯誤
import secrets
key = secrets.randbelow(1000000) # 只有 100 萬種可能!
# 正確
key = secrets.token_bytes(32) # 2^256 種可能錯誤 4:在虛擬機快照後不重新播種
VM 快照 → 恢復 → 產生「隨機」數
↑
和上次快照後產生的一樣!某些 CSPRNG 在 VM 恢復後需要重新收集熵。
錯誤 5:自己實作隨機數產生器
# 錯誤:「我設計了一個更好的演算法」
def my_random():
global state
state = (state * 31337 + 12345) ^ (time.time_ns() % 256)
return state
# 這幾乎肯定會有安全問題9. 測試隨機數品質
基本統計測試
import secrets
from collections import Counter
# 產生大量隨機數
samples = [secrets.randbelow(256) for _ in range(100000)]
# 檢查分佈
counter = Counter(samples)
for i in range(256):
expected = 100000 / 256
actual = counter[i]
if abs(actual - expected) / expected > 0.1: # 超過 10% 偏差
print(f"警告:值 {i} 的分佈異常")NIST 隨機數測試套件
# 使用 NIST SP 800-22 測試套件
# https://csrc.nist.gov/projects/random-bit-generation/documentation-and-software
./assess 1000000 # 測試 100 萬位元測試項目包括:
- 頻率測試(0 和 1 的比例)
- 區塊頻率測試
- 遊程測試
- 最長遊程測試
- 矩陣秩測試
- 離散傅立葉變換測試
- 等等…
10. 本章小結
三點要記住:
永遠使用 CSPRNG。 在任何涉及安全的場景——金鑰、IV、nonce、鹽值、token——都必須使用密碼學安全的隨機數產生器。Python 用
secrets,Node.js 用crypto.randomBytes,絕對不要用random或Math.random()。隨機數的品質決定加密的強度。 一個 256 位元的金鑰只有在每一位都是真正隨機的情況下才有 2^256 的安全性。如果產生器有缺陷,實際安全性可能只有 2^32 或更低。
重複使用是致命的。 在 ECDSA 中重複 nonce 會直接洩漏私鑰。在 AES-CTR 中重複 nonce 會讓攻擊者用 XOR 恢復明文。每個隨機數都必須是一次性的。
11. 下一步
我們已經完成了密碼學基礎的五個核心概念:加密不等於安全、密碼學解決的問題、對稱與非對稱加密、雜湊函式、以及隨機數。
在下一部分,我們將深入對稱加密的工程實踐:從 DES 說起——分組加密的基本思想,為什麼要分組,以及 DES 為什麼會失敗。
