为什么你不该「自己实现加密算法」
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)
索尼做了什么:
- 使用 ECDSA 签名游戏(正确的算法)
- ECDSA 要求每个签名使用随机 nonce k
- 索尼对每个签名使用相同的 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 主私钥被提取
- 任何人都可以签名"官方"游戏
- 整个安全模型崩溃
- 索尼损失数十亿案例 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、索尼、Debian——尽管有专家审查,都有密码学失败。你不比整个安全社区更聪明。
使用成熟的、经过审计的库。 唯一的获胜方式是不玩这个游戏。使用经过密码学专家审查、在生产中测试、并经受住攻击的库。
10. 下一步
理解为什么不要实现加密是第一步。但即使使用正确的库,系统仍然会失败。为什么?
在下一篇文章中:加密 ≠ 安全——密码学正确但其他一切都坏了的系统级失败。
