现代加密技术:这次怎么落地的

先复现,再谈优化。

这次项目要做数据加密,一开始觉得不就是用几个库的事,后来踩的坑让我重新理解了安全工程。

问题是怎么来的

项目要存储用户的敏感信息,包括身份证号、银行卡号等。合规要求这些数据必须加密存储,而且要满足前向保密——如果密钥泄露,历史数据不能被解密。

第一反应是用 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 模式下还能看出原图的大概轮廓,这是因为相同颜色区域加密出来的密文是一样的。

graph TB subgraph ECB模式 A1[Block 1] --> B1[加密] A2[Block 2] --> B2[加密] A3[Block 3] --> B3[加密] B1 --> C1[Cipher 1] B2 --> C2[Cipher 2] B3 --> C3[Cipher 3] end subgraph CBC模式 D1[Block 1] --> E1[XOR IV] D2[Block 2] --> E2[XOR Cipher 1] D3[Block 3] --> E3[XOR Cipher 2] E1 --> F1[加密] E2 --> F2[加密] E3 --> F3[加密] F1 --> G1[Cipher 1] F2 --> G2[Cipher 2] F3 --> G3[Cipher 3] end style B1 fill:#FFB6C1,stroke:#FF0000,stroke-width:2px style F1 fill:#90EE90

解决:换成 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/) 转载或引用必须申明原指尖魔法屋来源及源地址!