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 公钥是公开的,任何人都可以用它来重新加密被篡改的消息!

攻击者可以:

  1. 拦截消息;
  2. 解密 AES 密钥(因为你用了对称密钥加密消息);
  3. 解密消息内容;
  4. 修改内容;
  5. 再用 AES 加密;
  6. 使用相同 AES 密钥(它没变);
  7. 原封不动地附上 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流程:

image

RSA+AES+HMAC流程:

image

✅ 总结

这次升级后,我们的加密传输方案不仅实现了“加密”,还实现了“校验”和“认证”:

  • 防止消息被偷看(AES 加密)
  • 防止消息被篡改(HMAC 签名)
  • 防止消息被重放(时间戳 + nonce)

🔐 加密 ≠ 安全,完整性 + 认证性同样重要!
💡 如果你也在构建自己的通信协议,HMAC 是基础也是底线!

posted @ 2025-04-21 15:32  发光的反派  阅读(140)  评论(3)    收藏  举报