现代加密技术:这次怎么落地的
先复现,再谈优化。
这次项目要做数据加密,一开始觉得不就是用几个库的事,后来踩的坑让我重新理解了安全工程。
问题是怎么来的
项目要存储用户的敏感信息,包括身份证号、银行卡号等。合规要求这些数据必须加密存储,而且要满足前向保密——如果密钥泄露,历史数据不能被解密。
第一反应是用 AES 加密:
import os
from Crypto.Cipher import AES
def encrypt(data, key):
cipher = AES.new(key, AES.MODE_ECB)
return cipher.encrypt(data)
看起来很简单,但很快就被安全审查打回来了:ECB 模式不安全。
踩过的坑
坑一:ECB 模式不安全
被审查点出问题之后查了一下,ECB 模式的主要问题是:相同的明文块会产生相同的密文块,会泄露模式信息。
比如加密一张图片,ECB 模式下还能看出原图的大概轮廓,这是因为相同颜色区域加密出来的密文是一样的。
解决:换成 CBC 模式或 GCM 模式。GCM 是认证加密,同时保证机密性和完整性,更推荐。
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
def encrypt_gcm(data, key):
# GCM 模式需要 nonce,12 字节推荐
nonce = get_random_bytes(12)
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(data)
# 返回 nonce + tag + ciphertext
return nonce + tag + ciphertext
def decrypt_gcm(encrypted_data, key):
nonce = encrypted_data[:12]
tag = encrypted_data[12:28]
ciphertext = encrypted_data[28:]
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
return cipher.decrypt_and_verify(ciphertext, tag)
坑二:密钥硬编码
第一版代码的密钥直接写死在配置文件里:
# config.py
ENCRYPTION_KEY = b'this-is-a-32-byte-key-for-aes-256'
问题很明显:代码泄露等于密钥泄露,而且密钥轮换极其困难。
解决:使用密钥管理服务。
如果用云服务:
# AWS KMS
import boto3
kms = boto3.client('kms')
def generate_data_key():
response = kms.generate_data_key(
KeyId='alias/my-key',
KeySpec='AES_256'
)
return response['Plaintext'], response['CiphertextBlob']
如果自建,至少要把密钥放在环境变量或专门的密钥服务器里,不要硬编码。
坑三:密码哈希用错
用户登录功能的密码存储,一开始用 MD5:
import hashlib
def hash_password(password):
return hashlib.md5(password.encode()).hexdigest()
MD5 已经被证明不安全,彩虹表攻击可以快速破解。
解决:用专门设计来哈希密码的算法,如 Argon2、bcrypt 或 PBKDF2。
import bcrypt
def hash_password(password):
salt = bcrypt.gensalt()
return bcrypt.hashpw(password.encode(), salt)
def verify_password(password, hashed):
return bcrypt.checkpw(password.encode(), hashed)
关键点:
- 加入随机 salt,防止彩虹表攻击
- 可调的计算成本,增加暴力破解的难度
- 专门为密码哈希设计,不是通用的哈希算法
坑四:忽略了完整性验证
加密了数据,但没验证完整性。攻击者可以修改密文,虽然解密出来的不是原始数据,但可能会产生意外的行为。
# 错误:只有加密,没有完整性验证
def encrypt(data, key):
cipher = AES.new(key, AES.MODE_CBC)
return cipher.iv + cipher.encrypt(pad(data))
def decrypt(encrypted_data, key):
iv = encrypted_data[:16]
ciphertext = encrypted_data[16:]
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
return unpad(cipher.decrypt(ciphertext))
解决:使用认证加密(AEAD),如 GCM 模式,或者单独加 HMAC。
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import hmac
import hashlib
def encrypt_with_mac(data, key):
iv = get_random_bytes(16)
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
ciphertext = cipher.encrypt(pad(data))
# 计算 HMAC
mac = hmac.new(key, iv + ciphertext, hashlib.sha256).digest()
return iv + mac + ciphertext
def decrypt_with_mac(encrypted_data, key):
iv = encrypted_data[:16]
mac = encrypted_data[16:48]
ciphertext = encrypted_data[48:]
# 验证 MAC
expected_mac = hmac.new(key, iv + ciphertext, hashlib.sha256).digest()
if not hmac.compare_digest(mac, expected_mac):
raise ValueError("MAC verification failed")
cipher = AES.new(key, AES.MODE_CBC, iv=iv)
return unpad(cipher.decrypt(ciphertext))
实践中的加密方案
对称加密:大量数据加密
适合场景:数据库字段加密、文件加密、传输加密
推荐配置:
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
# 密钥长度:256 位(32 字节)
KEY_SIZE = 32
# Nonce 长度:12 字节(GCM 推荐)
NONCE_SIZE = 12
# Tag 长度:16 字节
TAG_SIZE = 16
class DataEncryptor:
def __init__(self, key: bytes):
if len(key) != KEY_SIZE:
raise ValueError(f"Key must be {KEY_SIZE} bytes")
self.key = key
def encrypt(self, plaintext: bytes) -> bytes:
nonce = get_random_bytes(NONCE_SIZE)
cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce)
ciphertext, tag = cipher.encrypt_and_digest(plaintext)
return nonce + tag + ciphertext
def decrypt(self, encrypted: bytes) -> bytes:
nonce = encrypted[:NONCE_SIZE]
tag = encrypted[NONCE_SIZE:NONCE_SIZE + TAG_SIZE]
ciphertext = encrypted[NONCE_SIZE + TAG_SIZE:]
cipher = AES.new(self.key, AES.MODE_GCM, nonce=nonce)
return cipher.decrypt_and_verify(ciphertext, tag)
非对称加密:密钥交换和数字签名
适合场景:TLS 握手、API 签名验证、密钥交换
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization
# 生成密钥对
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
# 加密(用公钥)
def encrypt_with_public(data: bytes, public_key):
ciphertext = public_key.encrypt(
data,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return ciphertext
# 解密(用私钥)
def decrypt_with_private(ciphertext: bytes, private_key):
plaintext = private_key.decrypt(
ciphertext,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA256()),
algorithm=hashes.SHA256(),
label=None
)
)
return plaintext
哈希:数据完整性校验
推荐算法:
- SHA-256:通用哈希
- SHA-3:新标准,抗量子攻击
- BLAKE3:高性能场景
import hashlib
def hash_data(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def hash_file(file_path: str) -> str:
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
安全检查清单
每次做加密相关的功能,对照这个清单检查一遍:
算法选择:
- 对称加密使用 AES-256-GCM
- 哈希使用 SHA-256 或更新算法
- 密码哈希使用 Argon2/bcrypt/PBKDF2
- 不使用 MD5、SHA-1、RC4、DES
密钥管理:
- 密钥不硬编码在代码中
- 密钥有轮换机制
- 密钥存储在安全的地方(HSM/KMS)
- 密钥有访问控制
完整性验证:
- 使用认证加密(GCM)或 HMAC
- 解密前验证完整性
- 使用恒定时间比较防止时序攻击
错误处理:
- 加密失败不泄露敏感信息
- 日志中不记录密钥或敏感数据
- 错误信息不帮助攻击者
什么时候该自己做
有些场景确实不需要上复杂的加密方案:
简单场景:
- 本地开发环境的临时加密
- 内部工具的数据混淆
- 非核心功能的安全需求
复杂场景:
- 用户敏感数据存储
- API 安全传输
- 金融、医疗等合规要求高的场景
复杂场景要么用成熟的库,要么找专业的人做,不要自己瞎搞。
写在最后
加密技术这东西,理论可以学,但实践要谨慎。
第一原则是永远不要自己实现加密算法。即使是配置和参数选择,也要遵循业界最佳实践,不要随意改动。
安全工程不是炫技的地方,目的是保护数据不被泄露。简单、可靠、可维护的方案,比花里胡哨但没人懂的方案更有价值。
这次项目做完,最大的收获不是掌握了多少加密算法,而是学会了敬畏——安全这东西,出了问题就晚了。
可用性说明:本文发布于 2018 年 4 月,距今已超过五年。文中涉及的软件版本、接口、下载地址、命令参数和操作界面可能已经发生变化,部分方案在当前环境下可能失效。请结合官方最新文档核对后再操作,生产环境使用前务必先行验证。
版权声明: 本文首发于 指尖魔法屋-现代加密技术:这次怎么落地的(https://blog.thinkmoon.cn/post/16-encryption-security-practice/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。