2. 字段级加密的核心原理:算法与工作模式
好,咱们直接切入正题。
字段级加密,说白了就是在数据库里挑出敏感字段,单独给它加密。比如身份证号、手机号、银行卡号——这些不能明文存。那加密到底怎么加?用啥算法?用什么模式?我踩过的坑不少,咱们一个一个说。
2.1 对称加密:AES 与国密 SM4
字段级加密基本都是对称加密。为什么?你想想看,数据要频繁读写,速度必须快。非对称加密慢得让人抓狂,不适合这种场景。
目前主流的对称加密算法就两个:AES 和 国密 SM4。
| 特性 | AES | SM4 |
|---|---|---|
| 分组长度 | 128 位 | 128 位 |
| 密钥长度 | 128/192/256 位 | 128 位 |
| 结构 | SP 网络 | Feistel 结构 |
| 轮数 | 10/12/14 轮 | 32 轮 |
| 典型应用 | 国际项目、通用场景 | 国产化合规、政务系统 |
AES 是国际标准,大家都熟。128位密钥足够安全,256位更安心。我个人习惯是:金融交易类用256位,普通业务字段128位也够。
SM4 是国家密码管理局发布的商用密码算法。它的结构跟 AES 不一样,用的是 Feistel 网络。安全强度对标 AES-128。我前几年做了个政务云项目,客户硬性要求用国密。当时 SM4 的 SDK 还没那么成熟,遇到不少兼容性问题。说白了,选 SM4 更多是合规驱动。
注意: 不要自己做算法实现。用成熟的密码库。AES 用 OpenSSL、Bouncy Castle。SM4 用 GmSSL、BabaSSL。自己实现等于给自己挖坑。
2.2 工作模式:ECB、CBC、GCM、CTR
算法定好了,还得选工作模式。这个很关键。模式不对,加密结果可能不安全,甚至限制业务。
2.2.1 ECB 模式:别碰
ECB 是最简单模式。每个分组独立加密,密钥相同。你猜会怎样?相同的明文块会生成相同的密文块。
我遇到过团队用 ECB 加密用户性别字段。结果密文只有两种值——猜都猜出来哪个是男哪个是女。这还加密干嘛?
结论: 字段级加密中,绝不要用 ECB 模式。
2.2.2 CBC 模式:传统但不完美
CBC 引入了初始化向量 IV。每个密文块会参与下一个块的加密。解决了 ECB 的重复问题。
但 CBC 有个麻烦:加密时必须按块处理。字段长度一般不是 128 位整数倍,需要 PKCS#7 填充。这就带来一个问题——密文长度会变。
// 看看长度变化
原始字段: "13800138000" (11 字节)
AES/CBC/PKCS7Padding 加密后: 16 字节
你想想看,数据库字段长度要预留。我见过有人没考虑这个,上线后字段截断,数据直接废了。
2.2.3 GCM 模式:我的首选
GCM 是 伽罗瓦计数器模式。它把 CTR 模式和 GMAC 认证组合在一起。一句话总结:既加密又做完整性校验。
- 不需要填充,密文长度等于明文长度。
- 提供认证标签,防止篡改。
- 支持并行计算,性能好。
我现在的项目,几乎都用 GCM。为什么?
首先,字段长度容易控制。存数据库时,VARCHAR 长度就是明文长度加上标签长度(16字节)。很清晰。
其次,安全性更高。CBC 只保证机密性。如果有人篡改密文,CBC 解密后是乱码,但你检测不到。GCM 能直接发现。
我的经验: GCM 里的 IV 不能重复。用 12 字节的随机 IV。每个字段每次加密都用新 IV。IV 重复会直接暴露密钥流。
2.2.4 CTR 模式:轻量选择
CTR 模式把块密码变成流密码。它也是将计数器加密后与明文异或。不支持认证,但效率高。
适合什么场景?你对性能要求极高,并且有独立的 MAC 校验机制。一般我建议业务方直接上 GCM,别在 CTR 上折腾。
2.3 算法与模式的选择矩阵
| 业务场景 | 推荐算法 | 推荐模式 | 核心考虑 |
|---|---|---|---|
| 国际化、通用业务 | AES-256 | GCM | 安全、兼容性好 |
| 国产化、政务、金融 | SM4 | GCM | 合规要求 |
| 高性能、短字段 | AES-128 | GCM | 速度和安全性平衡 |
| 遗留系统、只加密 | AES-256 | CBC | 兼容历史实现 |
2.4 核心流程:字段级加密的骨架
加密流程其实就几步。但每一步都有细节。我画张图,你一看就明白。
2.5 代码演示:Java 与 Python 的典型用法
我习惯用代码说话。给你两段核心实现,感受一下实际对接时的样子。
Java:AES-256-GCM 加密
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
public class FieldEncryptor {
private static final int GCM_TAG_LENGTH = 128; // 16字节
private static final int IV_LENGTH = 12; // 12字节
public static String encrypt(String plaintext, byte[] key) throws Exception {
byte[] iv = new byte[IV_LENGTH];
// 用 SecureRandom 生成随机 IV
new java.security.SecureRandom().nextBytes(iv);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
GCMParameterSpec spec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
SecretKeySpec keySpec = new SecretKeySpec(key, "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec, spec);
byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
// 输出格式:IV + 密文 + 认证标签(ciphertext已经包含标签)
byte[] combined = new byte[IV_LENGTH + ciphertext.length];
System.arraycopy(iv, 0, combined, 0, IV_LENGTH);
System.arraycopy(ciphertext, 0, combined, IV_LENGTH, ciphertext.length);
return Base64.getEncoder().encodeToString(combined);
}
}
Python:SM4-GCM 加密
from gmssl.sm4 import CryptSM4, SM4_GCM_MODE
import os
import base64
def sm4_gcm_encrypt(plaintext: str, key: bytes) -> str:
iv = os.urandom(12)
crypt_sm4 = CryptSM4()
crypt_sm4.set_key(key, SM4_GCM_MODE)
# GMSSL 的 GCM 接口会返回 (密文, 认证标签)
ciphertext, tag = crypt_sm4.sm4_crypt_gcm(iv, plaintext.encode('utf-8'))
# 组合 IV + 密文 + 标签
result = iv + ciphertext + tag
return base64.b64encode(result).decode('utf-8')
# 调用示例
key = b'\x00' * 16 # 生产环境从KMS获取
encrypted = sm4_gcm_encrypt('13800138000', key)
print(encrypted)
一个小坑: 我早期用 Python 做 SM4-GCM 时,发现不同库的标签拼接顺序不同。有的库 tag 放在密文前面。对接 SDK 时一定要确认编码格式。
2.6 关于 IV 的要点
- IV 不能重复: 同一个密钥下,IV 重复一次,安全就直接崩了。我曾经在项目中看到有人用固定 IV,被我当场叫停。
- IV 长度固定: GCM 推荐 12 字节。少于 12 字节会降低安全性。
- IV 存储: IV 不需要保密,但必须随密文一起存。一般拼在密文前面,解密时先取出来。
你可能会问:IV 能不能用自增计数器?理论上可以,但分布式环境里同步计数器太麻烦。直接用随机数,省心。