為什麼你不該「自己實現加密演算法」
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 9 minutes reading.
1. 為什麼要關心這個問題?
你理解 AES。你讀過 RSA。你知道數學是有效的。那為什麼不自己實現呢?
因為密碼學是唯一一個 99% 正確意味著 100% 失敗 的領域。
一個位元的時序差異。一個未檢查的錯誤條件。一個可預測的隨機數。這些中的任何一個都可以把你的「安全」系統變成攻擊者的歡迎墊。
2. 根本問題
密碼學沒有部分分數
普通軟體:
- 錯誤 → 錯誤輸出 → 使用者抱怨 → 你修復它
- 可見的、可除錯的、可修復的
密碼學軟體:
- 錯誤 → 看起來正確 → 攻擊者利用 → 資料洩露
- 靜默的、不可見的、災難性的
加密可能在你所有的測試中都「完美工作」
同時以你看不到的方式完全被破解。為什麼聰明人仍然失敗
密碼學安全取決於:
1. 數學正確性(簡單的部分)
2. 實現正確性(困難的部分)
3. 環境正確性(不可見的部分)
你可以在 #1 上得滿分,但在 #2 和 #3 上完全失敗。3. 歷史災難
案例 1:PlayStation 3 ECDSA 失敗(2010)
Sony 做了什麼:
- 使用 ECDSA 簽章遊戲(正確的演算法)
- ECDSA 要求每個簽章使用隨機 nonce k
- Sony 對每個簽章使用相同的 k
數學:
signature = (r, s) 其中 s = k⁻¹(hash + privateKey × r)
兩個使用相同 k 的簽章:
s₁ = k⁻¹(hash₁ + privateKey × r)
s₂ = k⁻¹(hash₂ + privateKey × r)
相減:
s₁ - s₂ = k⁻¹(hash₁ - hash₂)
k = (hash₁ - hash₂) / (s₁ - s₂)
一旦你有了 k:
privateKey = (s₁ × k - hash₁) / r
結果:
- PS3 主私鑰被擷取
- 任何人都可以簽章「官方」遊戲
- 整個安全模型崩潰
- Sony 損失數十億案例 2:Debian OpenSSL 災難(2008)
發生了什麼:
- Debian 維護者刪除了「未初始化記憶體」警告
- 刪除了兩行看起來像 bug 的程式碼
- 實際上刪除了唯一的熵源
「修復」:
// 之前(正確但觸發警告)
MD_Update(&m, buf, j); // 使用未初始化記憶體作為熵
MD_Update(&m, buf, j);
// 之後(損壞)
// 行被刪除因為 Valgrind 抱怨
結果:
- 2 年內所有在基於 Debian 系統上產生的金鑰
- 只能有 ~32,768 個可能的值(15 位元熵)
- 而不是 2^128 種可能性
- 兩年的 SSL 憑證、SSH 金鑰被洩露
- 需要大規模撤銷和重新產生案例 3:Cryptocat 加密缺陷(2013)
Cryptocat 做了什麼:
- 建構加密聊天應用
- 在 JavaScript 中實現自己的加密
- 在 ECC 實現中犯了一個錯誤
錯誤:
// 為橢圓曲線產生隨機值
// 使用 Math.random() 而不是 crypto.getRandomValues()
var random = Math.floor(Math.random() * max);
Math.random() 的特性:
- 不是密碼學安全的
- 給定足夠的樣本是可預測的
- 不同實現有不同的週期
結果:
- 私鑰可以被預測
- 所有「加密」訊息都可以被解密
- 依賴它的活動家和記者被暴露案例 4:WEP Wi-Fi 加密(1997-2004)
WEP 設計缺陷:
1. 24 位元 IV(初始化向量)太短
- 只有 1600 萬個可能的 IV
- 在繁忙的網路上重用是不可避免的
- 相同的 IV + 相同的金鑰 = 相同的金鑰流
2. IV 以明文發送
- 攻擊者知道 IV
- 可以收集具有相同 IV 的封包
- 將它們互斥或在一起以消除金鑰流
3. CRC32 用於完整性(不是密碼學的)
- 攻擊者可以修改封包
- 在不知道金鑰的情況下重新計算 CRC
- 沒有封包來源的認證
4. RC4 中的金鑰排程弱點
- 某些 IV 洩露金鑰位元
- 收集約 40,000 個帶有弱 IV 的封包
- 統計恢復金鑰
時間線:
1997:WEP 標準化
2001:第一個實用攻擊發布
2004:幾分鐘內完成完整金鑰恢復
2007:攻擊只需幾秒鐘
教訓:
- 短 IV 保證重用
- CRC 不是 MAC
- RC4 有統計偏差
- 「足夠好」的安全性不夠好4. 實現攻擊
時序攻擊
# 脆弱:早期退出暴露密碼長度/正確性
def check_password_bad(input_password, stored_hash):
if len(input_password) != len(stored_hash):
return False # 暴露長度!
for i in range(len(input_password)):
if input_password[i] != stored_hash[i]:
return False # 暴露第一個不匹配的位置!
return True
# 時序差異:
# 錯誤長度:~100ns
# 第一個字元錯誤:~150ns
# 第二個字元錯誤:~200ns
# ...攻擊者可以逐個字元推斷密碼
# 安全:常量時間比較
import hmac
def check_password_good(input_password, stored_hash):
return hmac.compare_digest(
input_password.encode(),
stored_hash.encode()
)
# 無論不匹配發生在哪裡,始終需要相同的時間填充預言攻擊
帶有 PKCS#7 填充的 CBC 模式加密:
┌─────────────────────────────────────────────────────────┐
│ 明文區塊在加密前被填充 │
│ │
│ "HELLO" → "HELLO\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b\x0b" │
│ (11 位元組的填充值 0x0b) │
└─────────────────────────────────────────────────────────┘
攻擊:
1. 向伺服器發送修改的密文
2. 伺服器解密,檢查填充
3. 如果伺服器對「填充錯誤」和「資料錯誤」回傳不同的錯誤...
4. 攻擊者可以逐位元組解密整個訊息!
脆弱的回應模式:
- 「填充錯誤」 vs 「解密失敗」(不同的訊息)
- 400 Bad Request vs 500 Internal Error(不同的狀態碼)
- 快速回應 vs 慢速回應(時序差異)
任何可觀察的差異都能啟用攻擊。
著名受害者:
- ASP.NET(2010):微軟的 Web 框架
- Java Server Faces(2010)
- Ruby on Rails(2013)
- 許多 TLS 實現側通道攻擊
側通道透過以下方式洩露資訊:
1. 時序
- 操作需要多長時間
- 快取命中/未命中模式
- 分支預測
2. 功耗
- 不同的操作使用不同的功率
- 可以用示波器測量
- 智慧卡特別脆弱
3. 電磁輻射
- CPU 發出無線電訊號
- 訊號隨操作變化
- 可以從幾公尺外測量
4. 聲音
- 電腦對不同操作發出不同的聲音
- RSA 金鑰從筆記型電腦冷卻風扇擷取
- 是的,真的(2013 年研究)
5. 錯誤訊息
- 不同條件的不同錯誤
- 填充預言是側通道
- 「無效使用者名稱」 vs 「無效密碼」
6. 快取時序
- 記憶體存取模式
- Spectre/Meltdown 利用了這一點
- 影響所有現代 CPU5. 為什麼即使是專家也會失敗
OpenSSL 心臟出血漏洞(2014)
// 簡化的脆弱程式碼
struct heartbeat_message {
uint8_t type;
uint16_t payload_length; // 攻擊者控制!
uint8_t payload[];
};
// 錯誤:信任使用者提供的長度
void process_heartbeat(struct heartbeat_message *msg) {
// 根據宣稱的長度分配回應緩衝區
response = malloc(msg->payload_length);
// 複製宣稱數量的位元組
memcpy(response, msg->payload, msg->payload_length);
// 發送回應
send(response, msg->payload_length);
}
// 攻擊:
// 攻擊者發送:payload_length = 65535,實際 payload = 1 位元組
// 伺服器從記憶體複製 65535 位元組(大部分不是 payload)
// 伺服器發回 65535 位元組包括:
// - 私鑰
// - 會話 cookie
// - 密碼
// - 其他使用者的資料
// 這在生產 OpenSSL 中存在了 2 年
// 由經驗豐富的安全開發人員編寫
// 被許多人審查
// 仍然被遺漏教訓
OpenSSL 是:
- 由密碼學專家編寫
- 開源(許多審查者)
- 廣泛部署(經過實戰測試)
- 仍然有一個關鍵漏洞存在 2 年
如果 OpenSSL 專家錯過緩衝區溢位,
你憑什麼認為你能發現時序攻擊?6. 正確的方法
使用成熟的函式庫
# 不要實現 AES
# 使用經過稽核的函式庫
# Python: cryptography 函式庫
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = AESGCM.generate_key(bit_length=256)
aesgcm = AESGCM(key)
ciphertext = aesgcm.encrypt(nonce, plaintext, associated_data)
# 函式庫處理:
# - 常量時間操作
# - 正確的隨機數產生
# - 記憶體安全
# - 側通道抵抗
# - 標準的正確實現使用高階 API
# 不要自己組合原語
# 錯誤:DIY 認證加密
def encrypt_bad(key, plaintext):
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
# 填充?HMAC?順序?你會搞錯的。
ciphertext = cipher.encrypt(pad(plaintext))
mac = hmac.new(key, ciphertext, sha256).digest()
return iv + ciphertext + mac
# 正確:使用處理一切的 AEAD
from cryptography.fernet import Fernet
key = Fernet.generate_key()
f = Fernet(key)
token = f.encrypt(plaintext)
# Fernet 處理:金鑰衍生、IV、加密、認證當你必須使用底層時
# 如果你絕對必須使用底層原語:
# 1. 使用 hazmat 模組(名字就是警告!)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.hkdf import HKDF
# 2. 嚴格遵循標準(RFC、NIST)
# 3. 從專業人士那裡獲得安全稽核
# 4. 假設你犯了錯誤
# 5. 準備好事件回應7. 加密程式碼中的危險訊號
警告標誌
# 🚨 危險訊號:自訂加密演算法
def my_encrypt(data, key):
result = []
for i, byte in enumerate(data):
result.append(byte ^ key[i % len(key)])
return bytes(result)
# 這是用重複金鑰的互斥或。自 1800 年代以來就已被破解。
# 🚨 危險訊號:使用 ECB 模式
cipher = AES.new(key, AES.MODE_ECB) # ECB 幾乎從不正確
# 🚨 危險訊號:使用 MD5 或 SHA1 用於安全
hash = hashlib.md5(password).hexdigest() # 已破解
hash = hashlib.sha1(password).hexdigest() # 已棄用
# 🚨 危險訊號:使用 random 而不是 secrets
import random # 不是密碼學安全的
key = bytes([random.randint(0, 255) for _ in range(32)])
# 🚨 危險訊號:用 == 比較金鑰
if token == expected: # 時序攻擊!
grant_access()
# 🚨 危險訊號:重用 nonce/IV
iv = b"constant_iv_1234" # 每次加密必須唯一!
# 🚨 危險訊號:加密但不認證
ciphertext = aes_encrypt(key, plaintext) # 沒有完整性檢查!
# 🚨 危險訊號:「我改進了演算法」
def improved_aes(data, key):
# 加入我自己的變化...
# 不要。停下。你在削弱它。8. 你應該做什麼
決策樹
你需要加密嗎?
│
├─ 用於靜態資料?
│ └─ 使用你平台的安全儲存
│ - iOS Keychain
│ - Android Keystore
│ - Windows DPAPI
│ - 雲端 KMS
│
├─ 用於傳輸中的資料?
│ └─ 使用 TLS
│ - 不要自己實現
│ - 使用你語言的標準函式庫
│ - 讓基礎設施處理它
│
├─ 用於密碼?
│ └─ 使用密碼雜湊
│ - Argon2id
│ - bcrypt
│ - 永遠不要加密,始終雜湊
│
├─ 用於權杖/會話?
│ └─ 使用成熟的函式庫
│ - JWT 函式庫(小心使用)
│ - 會話管理框架
│ - OAuth/OIDC 函式庫
│
└─ 用於自訂的東西?
└─ 諮詢密碼學專家
- 獲得專業稽核
- 使用成熟的建構區塊
- 準備好它可能是錯誤的值得信任的函式庫
通用:
- libsodium (NaCl) - 易於使用,難以誤用
- OpenSSL/BoringSSL - 經過實戰測試(儘管有 bug)
Python:
- cryptography - 現代、維護良好
- PyNaCl - libsodium 的 Python 繫結
JavaScript:
- Web Crypto API - 瀏覽器內建
- noble-* 函式庫 - 經過稽核、現代
Go:
- crypto/* - 標準函式庫,優秀
- golang.org/x/crypto - 擴展演算法
Rust:
- ring - 源自 BoringSSL
- RustCrypto - 純 Rust 實現9. 本章小結
三點要記住:
密碼學實現是不可原諒的。 一個時序差異、一個可預測的位元、一個未檢查的錯誤——你的安全就沒了。演算法可以是完美的,而實現是損壞的。
即使是專家也經常失敗。 OpenSSL、Sony、Debian——儘管有專家審查,都有密碼學失敗。你不比整個安全社群更聰明。
使用成熟的、經過稽核的函式庫。 唯一的獲勝方式是不玩這個遊戲。使用經過密碼學專家審查、在生產中測試、並經受住攻擊的函式庫。
10. 下一步
理解為什麼不要實現加密是第一步。但即使使用正確的函式庫,系統仍然會失敗。為什麼?
在下一篇文章中:加密 ≠ 安全——密碼學正確但其他一切都壞了的系統級失敗。
