RSA+AES+HMAC加密
RSA+AES+HMAC加密
加密 ≠ 安全?我为什么还要加上 HMAC!
在上一篇博客中,我用 AES + RSA 实现了一个加密通信方案,看上去已经非常安全了:
- 随机生成 AES 密钥加密消息内容;
- 用 RSA 私钥加密 AES 密钥;
- 把加密后的 AES 密钥和密文一起发送;
- 接收端用 RSA 公钥解密出 AES 密钥,再解密消息内容。
一切看起来都没问题,直到我意识到——别人可以修改我的消息,我却察觉不到!
安全方案一:只有加密,没有验证
我们先来看下我第一篇博客中使用的加密流程:
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 生成随机 AES 密钥 | 用于加密消息内容 |
| 2 | 使用 AES 密钥加密消息 | 确保消息内容不可读 |
| 3 | 用 RSA 私钥加密 AES 密钥 | 发送给接收方解密用 |
| 4 | 发送密文 + 加密的 AES 密钥 | 组合后发送给接收方 |
| 5 | 接收方用 RSA 公钥解密 AES 密钥 | 获取解密密钥 |
| 6 | 接收方用 AES 解密消息 | 还原消息内容 |
这个流程能保护消息不被“看见”,但不能防止消息被“改掉”。
问题:RSA 公钥是公开的!
我忽略了一个事实:
RSA 公钥是公开的,任何人都可以用它来重新加密被篡改的消息!
攻击者可以:
- 拦截消息;
- 解密 AES 密钥(因为你用了对称密钥加密消息);
- 解密消息内容;
- 修改内容;
- 再用 AES 加密;
- 使用相同 AES 密钥(它没变);
- 原封不动地附上 AES 密钥,发送出去。
接收端毫无察觉,成功解密——但内容早就被改了。
解决方案:加上 HMAC 进行完整性验证
为了解决这个问题,我加入了 HMAC(Hash-based Message Authentication Code),它能验证消息是否被改动,并确认是由我发出的。
✅ 更新后的加密流程:
| 步骤 | 动作 | 说明 |
|---|---|---|
| 1 | 生成 AES 密钥和 HMAC 密钥(可相同) | 用于加密消息和验证完整性 |
| 2 | 使用 AES 密钥加密消息内容 | 防止被窃取内容 |
| 3 | 使用 HMAC 密钥生成 HMAC(明文) |
防止消息被篡改 |
| 4 | 用 RSA 私钥加密 AES 密钥 | 保护传输过程中的密钥安全 |
| 5 | 用 RSA 私钥加密 HMAC 值 | 防止 HMAC 被伪造 |
| 6 | 发送密文 + 加密的 AES 密钥 + 加密的 HMAC | 一起发送给接收方 |
| 7 | 接收方用 RSA 公钥解密 AES 密钥 | 获取用于解密的密钥 |
| 8 | 解密密文,还原消息内容 | 得到原始消息 |
| 9 | 用解密出来的密钥重新生成 HMAC | 与收到的 HMAC 比较 |
| 10 | 比对 HMAC 是否一致 | 不一致则丢弃消息 |
📊 三种方案对比表
| 对比项 | 第一版:只用 AES | 第二版:RSA + AES | 第三版(本篇):RSA + AES + HMAC |
|---|---|---|---|
| 密钥生成 | 固定密钥 | 随机 AES 密钥 | 随机 AES 密钥 + HMAC 密钥 |
| 加密方式 | 只用 AES 加密消息 | AES 加密消息 + RSA 加密密钥 | AES 加密消息 + RSA 加密密钥 + HMAC |
| 是否防止被看见 | ✅ 是 | ✅ 是 | ✅ 是 |
| 是否防止被改写 | ❌ 否 | ❌ 否 | ✅ 是 |
| 是否能验证身份 | ❌ 否 | ❌ 否 | ✅ 是(HMAC) |
| 是否防重放攻击 | ❌ 否 | ❌ 否 | ✅ 是(timestamp + nonce) |
🧩 加密端 Java 示例(客户端)
public static EncryptedMessage encryptMessage(Map<String, Object> dataMap, String publicKeyBase64) throws Exception {
// 添加 timestamp 和 nonce
dataMap.put("timestamp", System.currentTimeMillis());
dataMap.put("nonce", UUID.randomUUID().toString());
// 公钥和 AES 密钥
PublicKey privateKey = RSAUtil.getPrivateKeyFromBase64(publicKeyBase64);
SecretKey aesKey = AESUtils.generateAESKey();
// 将 map 转 JSON 字符串
String jsonData = objectMapper.writeValueAsString(dataMap);
// AES 加密数据
String encryptedData = AESUtils.encryptAESKey(jsonData, aesKey);
// 生成 HMAC(对 JSON)
String hmac = HmacUtil.generateHMAC(jsonData, aesKey);
// RSA 加密 AES 密钥 和 HMAC
String encryptedAESKey = RSAUtil.encryptAESKey(aesKey, privateKey);
String encryptedHMAC = RSAUtil.encryptHMAC(hmac, privateKey);
return new EncryptedMessage(encryptedData, encryptedAESKey, encryptedHMAC);
}
🔓 解密端 Java 示例(服务端)
public static Map<String, Object> decryptMessage(String encryptedData, String encryptedKey, String encryptedHMAC, String privateKeyBase64) throws Exception {
// 解析私钥
PrivateKey publicKey = RSAUtil.getPublicKeyFromBase64(privateKeyBase64);
// 解密 AES 密钥
SecretKey aesKey = RSAUtil.decryptAESKey(encryptedKey, publicKey);
// 解密密文
String decryptedJson = AESUtils.decryptAESKey(encryptedData, aesKey);
// 解密 HMAC
String decryptedHMAC = RSAUtil.decryptHMAC(encryptedHMAC, publicKey);
// 验证 HMAC
if (!HmacUtil.verifyHMAC(decryptedJson, aesKey, decryptedHMAC)) {
throw new SecurityException("HMAC 校验失败!");
}
// JSON 解析
Map<String, Object> dataMap = objectMapper.readValue(decryptedJson, new TypeReference<>() {});
long timestamp = ((Number) dataMap.get("timestamp")).longValue();
// 防重放(5 分钟有效)
if (System.currentTimeMillis() - timestamp > 5 * 60 * 1000) {
throw new SecurityException("请求已过期!");
}
String nonce = (String) dataMap.get("nonce");
if (isNonceUsed(nonce)) {
throw new SecurityException("重复请求!");
}
storeNonce(nonce);
return dataMap;
}
流程图对比
RSA+AES流程:

RSA+AES+HMAC流程:

✅ 总结
这次升级后,我们的加密传输方案不仅实现了“加密”,还实现了“校验”和“认证”:
- 防止消息被偷看(AES 加密)
- 防止消息被篡改(HMAC 签名)
- 防止消息被重放(时间戳 + nonce)
🔐 加密 ≠ 安全,完整性 + 认证性同样重要!
💡 如果你也在构建自己的通信协议,HMAC 是基础也是底线!

浙公网安备 33010602011771号