对称加密在真实系统中的用法
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 9 minutes reading.
1. 为什么要关心这个问题?
你已经知道 AES-GCM 是好的选择,ECB 是灾难。但当你面对真实的工程问题时:
- 「我应该把 IV 存在哪里?」
- 「密钥怎么生成和存储?」
- 「我需要加密多少次?」
- 「性能会是问题吗?」
这些问题在教科书里很少提到,但在生产环境中会决定你的系统是安全的还是脆弱的。
2. HTTPS/TLS 中的对称加密
为什么 HTTPS 使用对称加密
TLS 握手使用非对称加密来交换密钥。但一旦握手完成,所有数据传输都用对称加密。为什么?
非对称加密(RSA-2048):~1 MB/s
对称加密(AES-256-GCM):~1 GB/s
相差 1000 倍!TLS 1.3 的对称加密阶段
┌─────────────────────────────────────────────────────────────┐
│ TLS 1.3 握手后 │
├─────────────────────────────────────────────────────────────┤
│ 客户端 → 服务器: │
│ Application Data │
│ encrypted with client_application_traffic_secret │
│ using AES-256-GCM or ChaCha20-Poly1305 │
├─────────────────────────────────────────────────────────────┤
│ 服务器 → 客户端: │
│ Application Data │
│ encrypted with server_application_traffic_secret │
│ using AES-256-GCM or ChaCha20-Poly1305 │
└─────────────────────────────────────────────────────────────┘TLS 的密钥派生
TLS 不直接使用交换的密钥。它使用 HKDF(HMAC-based Key Derivation Function)派生多个密钥:
Master Secret
│
├──► client_handshake_traffic_secret
├──► server_handshake_traffic_secret
├──► client_application_traffic_secret
└──► server_application_traffic_secret
每个方向有独立的密钥
防止反射攻击TLS 的 Nonce 管理
TLS 1.3 使用隐式 nonce:
nonce = static_IV XOR record_sequence_number
record_sequence_number 从 0 开始递增
每个连接的 static_IV 不同
保证 nonce 不重复3. 文件加密的最佳实践
基本架构
原始文件
│
▼
┌─────────────────────────────────────────────┐
│ 1. 生成随机 DEK(Data Encryption Key) │
├─────────────────────────────────────────────┤
│ 2. 用 DEK 加密文件(AES-GCM) │
├─────────────────────────────────────────────┤
│ 3. 用 KEK(Key Encryption Key)加密 DEK │
├─────────────────────────────────────────────┤
│ 4. 存储:加密的 DEK + IV + 加密的文件 │
└─────────────────────────────────────────────┘为什么需要两层密钥
只用一个密钥:
- 换密钥需要重新加密所有文件
- 密钥泄漏 = 所有数据泄漏
两层密钥(DEK + KEK):
- 每个文件有自己的 DEK
- 只需要重新加密 DEK(很小)
- 可以实现密钥轮换而不动数据实现示例
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.scrypt import Scrypt
import os
import json
import base64
class FileEncryptor:
def __init__(self, password: str):
"""从密码派生 KEK"""
self.salt = os.urandom(16)
self.kek = self._derive_kek(password, self.salt)
def _derive_kek(self, password: str, salt: bytes) -> bytes:
"""使用 scrypt 从密码派生密钥"""
kdf = Scrypt(
salt=salt,
length=32,
n=2**20, # CPU/内存成本
r=8,
p=1,
)
return kdf.derive(password.encode())
def encrypt_file(self, plaintext: bytes) -> dict:
"""加密文件"""
# 1. 生成随机 DEK
dek = AESGCM.generate_key(bit_length=256)
# 2. 用 DEK 加密数据
data_nonce = os.urandom(12)
data_cipher = AESGCM(dek)
encrypted_data = data_cipher.encrypt(data_nonce, plaintext, None)
# 3. 用 KEK 加密 DEK
key_nonce = os.urandom(12)
key_cipher = AESGCM(self.kek)
encrypted_dek = key_cipher.encrypt(key_nonce, dek, None)
# 4. 打包结果
return {
'version': 1,
'salt': base64.b64encode(self.salt).decode(),
'key_nonce': base64.b64encode(key_nonce).decode(),
'encrypted_dek': base64.b64encode(encrypted_dek).decode(),
'data_nonce': base64.b64encode(data_nonce).decode(),
'encrypted_data': base64.b64encode(encrypted_data).decode(),
}
def decrypt_file(self, encrypted: dict) -> bytes:
"""解密文件"""
# 解码
key_nonce = base64.b64decode(encrypted['key_nonce'])
encrypted_dek = base64.b64decode(encrypted['encrypted_dek'])
data_nonce = base64.b64decode(encrypted['data_nonce'])
encrypted_data = base64.b64decode(encrypted['encrypted_data'])
# 1. 解密 DEK
key_cipher = AESGCM(self.kek)
dek = key_cipher.decrypt(key_nonce, encrypted_dek, None)
# 2. 解密数据
data_cipher = AESGCM(dek)
plaintext = data_cipher.decrypt(data_nonce, encrypted_data, None)
return plaintext大文件处理
对于 GB 级的大文件,你不能把整个文件读入内存:
def encrypt_large_file(input_path: str, output_path: str, key: bytes):
"""流式加密大文件"""
CHUNK_SIZE = 64 * 1024 # 64KB chunks
# 使用 AES-GCM-SIV 或 AES-CTR + HMAC
# 注意:标准 AES-GCM 不适合流式加密,因为需要完整数据计算 tag
# 更好的选择:使用专门设计的文件加密格式
# 如 age (https://age-encryption.org/)
pass建议:对于大文件,使用成熟的工具如 age 或 gpg,而不是自己实现。
4. 数据库加密
加密层级
┌─────────────────────────────────────────────────────────────┐
│ 1. 传输加密(TLS) │
│ - 加密客户端和数据库之间的通信 │
│ - 防止网络窃听 │
├─────────────────────────────────────────────────────────────┤
│ 2. 透明数据加密(TDE) │
│ - 数据库文件层级加密 │
│ - 防止磁盘被盗 │
│ - 对应用程序透明 │
├─────────────────────────────────────────────────────────────┤
│ 3. 字段级加密 │
│ - 应用程序层级加密 │
│ - 只加密敏感字段 │
│ - 数据库管理员也看不到明文 │
└─────────────────────────────────────────────────────────────┘字段级加密示例
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os
import base64
class EncryptedField:
def __init__(self, key: bytes):
self.cipher = AESGCM(key)
def encrypt(self, value: str) -> str:
"""加密字段值"""
nonce = os.urandom(12)
ciphertext = self.cipher.encrypt(nonce, value.encode(), None)
# 格式:nonce + ciphertext,base64 编码
return base64.b64encode(nonce + ciphertext).decode()
def decrypt(self, encrypted_value: str) -> str:
"""解密字段值"""
data = base64.b64decode(encrypted_value)
nonce = data[:12]
ciphertext = data[12:]
plaintext = self.cipher.decrypt(nonce, ciphertext, None)
return plaintext.decode()
# 使用
field_key = os.urandom(32)
encrypted_field = EncryptedField(field_key)
# 存储到数据库
ssn = "123-45-6789"
encrypted_ssn = encrypted_field.encrypt(ssn)
# INSERT INTO users (encrypted_ssn) VALUES ('...')
# 从数据库读取
decrypted_ssn = encrypted_field.decrypt(encrypted_ssn)加密字段的查询问题
-- 这不能工作!
SELECT * FROM users WHERE encrypted_ssn = ?
-- 因为相同的明文会产生不同的密文(不同的 nonce)解决方案:
1. 盲索引(Blind Index)
- 对明文计算 HMAC
- 存储 HMAC 作为可搜索的索引
- 查询时计算 HMAC 进行比对
2. 确定性加密(Deterministic Encryption)
- 固定 nonce 或使用 SIV 模式
- 相同明文产生相同密文
- 可以精确匹配
- 会泄漏相等性信息
3. 同态加密(Homomorphic Encryption)
- 可以在密文上进行运算
- 性能开销很大
- 仍在研究阶段盲索引实现
import hmac
import hashlib
def create_blind_index(value: str, key: bytes) -> str:
"""创建可搜索的盲索引"""
h = hmac.new(key, value.encode(), hashlib.sha256)
# 只取前 16 字节,减少存储空间,增加一点模糊性
return base64.b64encode(h.digest()[:16]).decode()
# 使用
index_key = os.urandom(32) # 与加密密钥不同!
ssn = "123-45-6789"
ssn_index = create_blind_index(ssn, index_key)
# 存储
# INSERT INTO users (encrypted_ssn, ssn_index) VALUES (?, ?)
# 查询
search_index = create_blind_index("123-45-6789", index_key)
# SELECT * FROM users WHERE ssn_index = ?5. 密钥管理
密钥的生命周期
生成 → 分发 → 使用 → 轮换 → 撤销 → 销毁
每个阶段都有安全考量:
- 生成:必须使用 CSPRNG
- 分发:必须安全传输
- 使用:必须限制访问
- 轮换:必须支持多版本
- 撤销:必须快速生效
- 销毁:必须不可恢复密钥存储选项
┌─────────────────────────────────────────────────────────────┐
│ 开发环境 │
│ - 环境变量 │
│ - 配置文件(不要提交到 Git!) │
├─────────────────────────────────────────────────────────────┤
│ 生产环境 │
│ - 云端密钥管理服务(AWS KMS、GCP KMS、Azure Key Vault) │
│ - HashiCorp Vault │
│ - 硬件安全模块(HSM) │
└─────────────────────────────────────────────────────────────┘使用云端 KMS 的示例
import boto3
class KMSKeyManager:
def __init__(self, key_id: str):
self.kms = boto3.client('kms')
self.key_id = key_id
def generate_data_key(self) -> tuple:
"""生成数据密钥"""
response = self.kms.generate_data_key(
KeyId=self.key_id,
KeySpec='AES_256'
)
return (
response['Plaintext'], # 用于加密
response['CiphertextBlob'] # 存储这个
)
def decrypt_data_key(self, encrypted_key: bytes) -> bytes:
"""解密数据密钥"""
response = self.kms.decrypt(
KeyId=self.key_id,
CiphertextBlob=encrypted_key
)
return response['Plaintext']
# 使用
km = KMSKeyManager('alias/my-key')
# 加密时
plaintext_key, encrypted_key = km.generate_data_key()
# 用 plaintext_key 加密数据
# 存储 encrypted_key 和加密的数据
# 解密时
plaintext_key = km.decrypt_data_key(encrypted_key)
# 用 plaintext_key 解密数据6. 性能考量
硬件加速
# 检查 CPU 是否支持 AES-NI
import subprocess
result = subprocess.run(['grep', 'aes', '/proc/cpuinfo'], capture_output=True)
has_aesni = b'aes' in result.stdout
# 现代 CPU 几乎都支持
# AES-NI 可以让 AES 加密快 10 倍以上加密对性能的影响
操作 | 没有加密 | AES-256-GCM
-------------------------------------------
文件读写 | 1.0x | ~1.1x
网络传输 | 1.0x | ~1.05x
数据库查询 | 1.0x | 1.0x(TDE)
字段加密/解密 | 1.0x | ~1.5-2x
结论:对于大多数应用,加密的性能开销可以忽略何时性能会是问题
1. 大量小文件
- 每次加密都需要初始化
- 考虑批处理
2. 实时数据流
- 延迟敏感
- 考虑 ChaCha20-Poly1305(没有 AES-NI 时更快)
3. 数据库字段加密 + 大量查询
- 每次访问都需要加解密
- 考虑缓存解密后的值7. 常见错误总结
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 把密钥硬编码在代码中 | 密钥随代码外泄 | 使用环境变量或密钥管理服务 |
| 使用密码直接当密钥 | 密钥空间太小 | 使用 KDF(PBKDF2、scrypt、Argon2) |
| 不存储 IV/nonce | 无法解密 | IV 可以和密文一起存储 |
| 把 IV 存成秘密 | 没必要,反而增加复杂性 | IV 不需要保密,只需要唯一 |
| 用同一个密钥加密太多数据 | GCM 有数据量限制 | 定期轮换密钥 |
| 自己实现加密逻辑 | 几乎肯定有漏洞 | 使用成熟的函数库 |
8. 本章小结
三点要记住:
HTTPS 展示了对称加密的最佳实践。 密钥派生、nonce 管理、认证加密——TLS 的设计值得学习。
文件加密用双层密钥(DEK + KEK)。 这让密钥轮换变得简单,也提供了更好的安全隔离。
数据库加密有多个层级。 传输加密、透明数据加密、字段加密各有用途。字段加密要考虑查询的问题。
9. 下一步
我们已经完成了对称加密的深度探索。但对称加密有一个根本问题:双方需要预先共享密钥。
在下一部分,我们将进入非对称加密的世界:RSA 的核心思想——为什么「分解大数」这么难,以及公钥和私钥是怎么来的。
