2958 字
约 9 分钟
13
S02-X06 · 对称与非对称加密:密钥分发问题与混合加密

"用 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 解决"公钥归属"的可信

混合加密的标准编排只有三步,所有协议都是它的变体:

  1. 双方用非对称算法(RSA 加密,或 ECDHE 交换)协商出一把临时对称密钥;
  2. 用这把密钥 + AEAD 模式加密全部业务数据;
  3. 会话结束丢弃临时密钥(前向保密就来自"用完即弃")。
# 完整握手走一遍,看协商出的套件与密钥交换算法
$ 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. 自测

  1. 为什么"RSA 密钥 2048 位比 AES-128 更安全"是错误推理?两者对应的安全强度大致是多少?
  2. GCM 下同一个密钥重用 IV 会发生什么?为什么它比 CBC 的 IV 复用更致命?
  3. 混合加密的三步编排是什么?前向保密靠哪一步实现?
  4. 数字签名与加密分别用谁的哪把钥匙?为什么签名不提供机密性?
  5. "浏览器能打开、Java 客户端报 PKIX 错误"最常见的原因是什么?
  6. 为什么 SHA-256(secret + msg) 不能当 MAC 用?应该换成什么?
S02-X06 · 对称与非对称加密:密钥分发问题与混合加密
https://www.clxhxhhr.top/posts/5796/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。