企业微信会话存档RSA解密实战:从官方文档到可运行代码
1. 企业微信会话存档解密的核心挑战第一次接触企业微信会话存档功能时我盯着官方文档里那段关于RSA解密的说明看了足足十分钟。文档说要对encrypt_random_key进行Base64解码然后用PKCS1格式的私钥解密但具体怎么操作却只字未提。这感觉就像给你一张藏宝图上面只写了往东走却没告诉你具体要走多少步。最让人头疼的是三个技术点首先是Base64解码这个还算简单其次是PKCS1格式的私钥加载这个在Java里特别麻烦最后是RSA解密本身参数配置不对就会报错。我当时试了各种方法要么抛出InvalidKeyException要么解密出来是乱码整整折腾了一天。2. 解密流程全解析2.1 理解加密数据流企业微信的加密机制其实很典型先用随机生成的对称密钥加密实际消息内容再用RSA加密这个随机密钥。所以我们要做的第一步就是解密这个被RSA加密过的随机密钥。具体数据流是这样的企业微信生成随机对称密钥比如AES密钥用这个密钥加密实际聊天消息用我们预先配置的RSA公钥加密这个随机密钥将加密后的随机密钥做Base64编码放在encrypt_random_key字段把加密后的消息和加密后的随机密钥一起发送给我们2.2 准备PKCS1格式私钥这里有个大坑Java默认不支持PKCS1格式的私钥。官方文档里说的使用消息指明版本的私钥其实就是指PKCS1格式的私钥。我试过直接用PKCS8格式的私钥结果死活解不开。正确的私钥格式应该是这样的-----BEGIN RSA PRIVATE KEY----- BASE64编码的PKCS1私钥 -----END RSA PRIVATE KEY-----如果你的私钥是PKCS8格式开头是-----BEGIN PRIVATE KEY-----需要先用OpenSSL转换openssl rsa -in private_pkcs8.pem -out private_pkcs1.pem3. Java实现完整解密流程3.1 加载PKCS1私钥这是整个过程中最复杂的部分。Java的KeyFactory不支持直接加载PKCS1格式的私钥我们需要手动解析DER编码public static PrivateKey loadPKCS1PrivateKey(String privateKeyPEM) throws Exception { // 去掉头尾标记和换行符 String privateKeyContent privateKeyPEM .replace(-----BEGIN RSA PRIVATE KEY-----, ) .replace(-----END RSA PRIVATE KEY-----, ) .replaceAll(\\s, ); // Base64解码 byte[] privateKeyDER Base64.getDecoder().decode(privateKeyContent); // 手动解析DER格式 DerInputStream derReader new DerInputStream(privateKeyDER); DerValue[] seq derReader.getSequence(0); // 提取RSA参数 BigInteger modulus seq[1].getBigInteger(); BigInteger publicExp seq[2].getBigInteger(); BigInteger privateExp seq[3].getBigInteger(); BigInteger prime1 seq[4].getBigInteger(); BigInteger prime2 seq[5].getBigInteger(); BigInteger exp1 seq[6].getBigInteger(); BigInteger exp2 seq[7].getBigInteger(); BigInteger crtCoef seq[8].getBigInteger(); // 构建密钥规范 RSAPrivateCrtKeySpec keySpec new RSAPrivateCrtKeySpec( modulus, publicExp, privateExp, prime1, prime2, exp1, exp2, crtCoef); KeyFactory keyFactory KeyFactory.getInstance(RSA); return keyFactory.generatePrivate(keySpec); }3.2 解密encrypt_random_key拿到私钥后解密过程就简单了public static String decryptRandomKey(String encryptRandomKey, PrivateKey privateKey) throws Exception { // Base64解码 byte[] encryptedData Base64.getDecoder().decode(encryptRandomKey); // 配置RSA解密器 Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.DECRYPT_MODE, privateKey); // 执行解密 byte[] decryptedData cipher.doFinal(encryptedData); return new String(decryptedData, StandardCharsets.UTF_8); }注意这里必须明确指定RSA/ECB/PKCS1Padding如果只写RSA可能会使用默认的填充方式导致解密失败。4. 实战中的常见问题排查4.1 密钥格式错误最常见的错误就是私钥格式不对。我建议在代码开头先检查私钥格式if (!privateKeyPEM.contains(BEGIN RSA PRIVATE KEY)) { throw new IllegalArgumentException(必须是PKCS1格式的私钥); }4.2 解密结果乱码如果解密出来的随机密钥是乱码可能是私钥不匹配不是当初配置的公钥对应的私钥Base64解码出错有些Base64实现需要处理换行符字符集问题确保使用UTF-84.3 性能优化建议RSA解密比较耗性能特别是在高并发场景下。我有两个优化建议缓存PrivateKey对象不要每次解密都重新加载考虑使用线程安全的Cipher对象池5. 完整代码示例下面是一个完整的工具类实现import java.math.BigInteger; import java.security.*; import java.security.spec.*; import java.util.Base64; import java.nio.charset.StandardCharsets; import sun.security.util.DerInputStream; import sun.security.util.DerValue; public class WeChatWorkDecryptor { public static String decryptMessage(String encryptRandomKey, String encryptChatMsg, String privateKeyPEM) throws Exception { // 1. 加载私钥 PrivateKey privateKey loadPKCS1PrivateKey(privateKeyPEM); // 2. 解密随机密钥 String randomKey decryptRandomKey(encryptRandomKey, privateKey); // 3. 解密实际消息这里需要调用企业微信的SDK return DecryptData(randomKey, encryptChatMsg); } private static PrivateKey loadPKCS1PrivateKey(String privateKeyPEM) throws Exception { // 省略见上文实现... } private static String decryptRandomKey(String encryptRandomKey, PrivateKey privateKey) throws Exception { // 省略见上文实现... } // 这是企业微信SDK提供的解密方法 private static native String DecryptData(String randomKey, String encryptData); }使用时只需要调用decryptMessage方法传入三个参数encrypt_random_key字段值encrypt_chat_msg字段值PKCS1格式的私钥字符串6. 测试验证方案为了确保解密代码正确我建议按以下步骤验证使用在线工具生成PKCS1密钥对openssl genrsa -out private.pem 2048 openssl rsa -in private.pem -pubout -out public.pem用公钥加密测试字符串public static String encryptTestData(String data, PublicKey publicKey) throws Exception { Cipher cipher Cipher.getInstance(RSA/ECB/PKCS1Padding); cipher.init(Cipher.ENCRYPT_MODE, publicKey); byte[] encryptedData cipher.doFinal(data.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encryptedData); }用我们的解密代码解密验证是否能还原原始字符串7. 与官方SDK的集成虽然我们实现了核心解密逻辑但在生产环境中还是建议优先使用官方SDK。官方FinanceDemo中缺少解密实现的部分我们可以这样补充// 在FinanceDemo中添加解密工具类 public class DecryptUtil { public static String decrypt(Finance.NewChatData chatData, String privateKey) { try { return WeChatWorkDecryptor.decryptMessage( chatData.getEncryptRandomKey(), chatData.getEncryptChatMsg(), privateKey ); } catch (Exception e) { throw new RuntimeException(解密失败, e); } } }这样既保持了官方SDK的结构又补充了关键的解密功能。