"用 AES 还是 RSA"这个问题本身就问错了。两者解决的不是同一个问题:对称密码解决"怎么加密得快",非对称密码解决"密钥怎么安全地交到对方手里"。真实系统里几乎都是后者换出前者、再由前者搬运数据——HTTPS、SSH、Signal、磁盘加密的封装层都遵循同一套编排。
这篇按"机制 → 分工 → 编排 → 判据 → 坑"走一遍,代码以 JDK 17 的 JCA 与 OpenSSL 3.x 为准。
1. 对称加密:快,但密钥是个秘密
对称加密用同一把密钥加解密。算法分两类:分组密码把数据切成固定长度的块(AES 128 位块、SM4 128 位块),流密码按字节生成密钥流异或(ChaCha20)。
分组密码不能直接"一块一块独立加密",需要工作模式决定块之间怎么串:
| 模式 | 并行 | 需要 IV | 提供完整性 | 主要风险 |
|---|---|---|---|---|
| ECB | 可 | 不需要 | 否 | 相同明文块产生相同密文块,可辨结构,禁止用于真实数据 |
| CBC | 解密可并行 | 需要,且必须随机不可预测 | 否 | 填充预言攻击(padding oracle)、IV 复用 |
| CTR | 可 | 需要,且绝不可重用 | 否 | 密钥流重用即明文泄露 |
| GCM | 加密可并行 | 需要 12 字节 IV,每密钥只能用一个 IV | 是(AEAD) | IV 重用直接泄露认证密钥 |
| CCM | 否 | 需要 | 是(AEAD) | 实现较少 |
新代码基本只有一个答案:AES-GCM(或 ChaCha20-Poly1305,无 AES 硬件加速时更快)。它把"加密 + 认证"合成一个原语,省掉"先加密再算 MAC、还要小心 MAC-then-Encrypt 顺序"这些容易错的活。
# 口令派生的传统写法(仅演示机密性,无认证)
$ openssl enc -aes-256-cbc -pbkdf2 -iter 200000 -salt -in plain.txt -out cipher.bin -pass pass:demo
-pbkdf2 是从口令派生密钥,与"用现成密钥"是两件事。生产代码里别用口令当密钥,也别自己拼"盐 + IV + 密文"。
// JDK 17:AES-GCM 的标准形状
Cipher c = Cipher.getInstance("AES/GCM/NoPadding");
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv); // 每次加密都换
c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));
byte[] ct = c.doFinal(plain);
两个关键数字:IV 12 字节、tag 128 位。
2. 非对称加密:慢,但解决了"给钥匙"的问题
非对称加密有一对数学上关联的密钥:公钥可公开,私钥自己留着。它慢得多——RSA 私钥运算比 AES 慢几个数量级,所以从不用于批量数据。
| 维度 | RSA | ECC(如 P-256、X25519) |
|---|---|---|
| 数学难题 | 大整数分解 | 椭圆曲线离散对数 |
| 常用长度 | 2048 / 3072 / 4096 位 | 256 / 384 位 |
| 安全强度大致对应 | 2048 位 ≈ 112 位对称强度 | P-256 ≈ 128 位 |
| 公钥/签名体积 | 大(256 B 起) | 小(几十字节) |
| 运算开销 | 高,越长越明显 | 低 |
| 典型用途 | 兼容老系统、证书签名 | TLS 1.3 密钥交换、移动端 |
| 注意事项 | 必须 OAEP 填充,PKCS#1 v1.5 已有攻击面 | 曲线参数必须可信,禁止自造曲线 |
$ openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out rsa.key
$ openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out ec.key
$ openssl pkey -in ec.key -text -noout | head -5
两者的"位数"不是同一把尺子:2048 位 RSA 只相当于 112 位对称强度,AES-128 本身就是 128 位强度。
3. 分工:一个管速度,一个管分发
| 问题 | 由谁解决 | 说明 |
|---|---|---|
| 大量数据的机密性 | 对称加密 | 吞吐是硬指标 |
| 密钥怎么安全送到对方 | 非对称加密 / DH 密钥交换 | 只传一个短密钥或算出共享密钥 |
| 消息是否被改过 | 哈希 + MAC(HMAC)或 AEAD | 完整性是独立需求 |
| 消息是谁发的 | 数字签名 | 用私钥签、公钥验 |
| 公钥属于谁 | 证书链 / PKI | 解决"公钥归属"的可信 |
混合加密的标准编排只有三步,所有协议都是它的变体:
- 双方用非对称算法(RSA 加密,或 ECDHE 交换)协商出一把临时对称密钥;
- 用这把密钥 + AEAD 模式加密全部业务数据;
- 会话结束丢弃临时密钥(前向保密就来自"用完即弃")。
# 完整握手走一遍,看协商出的套件与密钥交换算法
$ openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| grep -E "Protocol|Cipher|Server Temp Key"
# 典型输出:TLSv1.3 / TLS_AES_128_GCM_SHA256 / X25519 253 bits
TLS 1.3 里 RSA 不再参与密钥交换,只用于签名;密钥交换统一走 (EC)DHE。所以"换更强的 RSA 密钥"对握手性能几乎无帮助。
4. 签名与加密方向相反
| 操作 | 输入 | 用哪把钥匙 | 谁能做 | 目的 |
|---|---|---|---|---|
| 加密 | 明文 | 接收方公钥 | 任何人 | 只有接收方能读 |
| 解密 | 密文 | 接收方私钥 | 接收方 | 取回明文 |
| 签名 | 消息摘要 | 发送方私钥 | 只有发送方 | 谁都能验,无法抵赖 |
| 验签 | 消息 + 签名 | 发送方公钥 | 任何人 | 确认来源与完整性 |
$ openssl dgst -sha256 -sign ec.key -out msg.sig msg.txt
$ openssl dgst -sha256 -verify <(openssl pkey -in ec.key -pubout) -signature msg.sig msg.txt
签名不提供机密性:签过名的消息仍是明文。
Signature s = Signature.getInstance("SHA256withECDSA");
s.initSign(privateKey); s.update(payload);
byte[] sig = s.sign();
5. 证书链在解决什么
公钥是公开的,但"这把公钥确实属于 example.com"这个断言需要有人背书。证书就是一个被 CA 签过名的结构:主体 + 公钥 + 有效期 + 用途,CA 用私钥对它签名,验证方用 CA 的公钥验签,逐级上溯到本地信任的根。
| 环节 | 验证什么 | 常见失败 |
|---|---|---|
| 签名链 | 每一级由上一级签发 | 中间证书缺失(服务器没发全) |
| 有效期 | notBefore / notAfter |
过期、容器时区错导致误判 |
| 主体名 | SAN 里是否含访问的域名 | 只写了 CN,现代客户端只看 SAN |
| 用途 | extendedKeyUsage 是否含 serverAuth |
拿客户端证书当服务端证书 |
$ openssl s_client -connect example.com:443 -showcerts </dev/null 2>/dev/null | grep -E "^ *[0-9] s:|i:"
$ openssl x509 -in cert.pem -noout -text | grep -A2 Alternative
链的验证由服务器发来的顺序 + 本地信任库共同决定。"浏览器能开、Java 客户端报 PKIX path building failed",最常见原因是服务器少发中间证书。
6. 哈希与 MAC:完整性不是机密性
| 原语 | 有密钥 | 能防篡改 | 能防伪造 | 用途 |
|---|---|---|---|---|
| 哈希(SHA-256) | 否 | 能发现意外损坏 | 不能,谁都能重算 | 校验和、内容寻址、口令派生 |
| HMAC(HMAC-SHA256) | 是 | 能 | 能(无密钥算不出) | 消息认证、API 签名 |
| AEAD(AES-GCM) | 是 | 能 | 能 | 加密并认证,一步到位 |
| 数字签名 | 私钥 | 能 | 能,且可公开验证 | 不可抵赖 |
单纯哈希不能替代 MAC:攻击者改了消息可以顺手重算哈希,接收方无从分辨。hash(secret + msg) 这种"自己拼的 MAC"还有长度扩展攻击问题,必须用标准 HMAC 构造。
$ openssl dgst -sha256 -hmac "shared-secret" msg.txt # HMAC,双方共享密钥
$ openssl dgst -sha256 msg.txt # 纯哈希,无密钥
比较 MAC 要用常量时间比较(MessageDigest.isEqual),逐字节提前返回会把"前几位匹配"泄露成旁路。
7. 选择判据
| 需求 | 选择 | 不该选 |
|---|---|---|
| 加密磁盘/数据库字段 | AES-256-GCM,密钥由 KMS 托管 | RSA 加密字段(体积与性能都不合适) |
| 传输层机密性 | TLS 1.3(内部已是混合加密) | 自己在应用层拼一套"RSA + AES" |
| 接口防篡改 | HMAC-SHA256 + 时间戳 + nonce | 只对参数做 SHA-256 |
| 软件发布包 | Ed25519 或 RSA-3072 签名 | 只给 SHA-256 校验和(可被同步替换) |
| 口令存储 | Argon2id / scrypt / PBKDF2 | AES 加密口令(可逆) |
口令那条:威胁模型是"库被拖走后离线爆破",要的是慢与加盐,不是可逆——加密口令意味着密钥一泄全泄。
8. 常见坑
| 坑 | 为什么错 | 正确做法 |
|---|---|---|
| 用 RSA 直接加密大文件 | 分块后每块都要一次私钥运算,且填充与分块极易写错 | 生成随机 AES 密钥加密文件,RSA 只加密这把密钥 |
| ECB 模式 | 相同明文块 → 相同密文块,图片能看出轮廓 | 换 GCM 或至少 CBC + 随机 IV |
| 固定/可预测 IV | CBC 下泄露首块明文关系,GCM 下 IV 重用直接崩盘 | 每次随机生成,随密文存 |
| 把哈希当加密 | 哈希不可逆,且不防伪造 | 机密性用 AEAD,完整性用 MAC/签名 |
MAC 用 == 比较 |
提前返回泄露前缀,构成时序旁路 | 常量时间比较 |
| 自造"加密方案" | MAC-then-Encrypt 等组合顺序已有大量攻击 | 用 AEAD 标准原语 |
| 密钥硬编码在配置里 | 仓库、镜像层、日志三处都可能泄露 | KMS / 环境注入 + 轮换 |
// 反例:ECB,相同明文块出相同密文块
Cipher.getInstance("AES/ECB/PKCS5Padding");
// 正例:GCM,认证加密
Cipher.getInstance("AES/GCM/NoPadding");
9. 边界:什么情况不该自己碰密码学
| 场景 | 更合适的做法 | 理由 |
|---|---|---|
| 需要"安全的 HTTP" | 直接上 TLS,不写应用层加密 | 协议层已经解决了密钥协商、前向保密、重放 |
| 只需要防误改 | CRC / SHA-256 校验和 | 无对抗场景,不必引入密钥管理 |
| 需要"只有我能看"的存储 | 磁盘/对象存储的服务端加密 | 密钥生命周期交给平台 |
| 需要在浏览器里做端到端 | 用成熟库(WebCrypto + 已审计封装) | 自实现的填充、IV、序列化都是坑 |
一句话:对称算法负责把所有数据变密,非对称算法只负责把那一把钥匙安全地递过去;任何让非对称去搬数据的方案,都是在用错误的工具付多余的成本。
10. 自测
- 为什么"RSA 密钥 2048 位比 AES-128 更安全"是错误推理?两者对应的安全强度大致是多少?
- GCM 下同一个密钥重用 IV 会发生什么?为什么它比 CBC 的 IV 复用更致命?
- 混合加密的三步编排是什么?前向保密靠哪一步实现?
- 数字签名与加密分别用谁的哪把钥匙?为什么签名不提供机密性?
- "浏览器能打开、Java 客户端报 PKIX 错误"最常见的原因是什么?
- 为什么
SHA-256(secret + msg)不能当 MAC 用?应该换成什么?