在聊企业微信会话内容解密之前,先说一说会话内容白名单遇到的问题,因为部署的服务器是不能连接外网的,所以需要通过服务器的另一台机器代理请求访问,先来看一下官方文档给出的描述
/**
* 拉取聊天记录函数
* Return值=0表示该API调用成功
*
*
* @param [in] sdk NewSdk返回的sdk指针
* @param [in] seq 从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0
* @param [in] limit 一次拉取的消息条数,最大值1000条,超过1000条会返回错误
* @param [in] proxy 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081
* @param [in] passwd 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123
* @param [in] timeout 超时时间,单位秒
* @param [out] chatDatas 返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。示例如下:
{"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]}
*
* @return 返回是否调用成功
* 0 - 成功
* !=0 - 失败
*/
int GetChatData(WeWorkFinanceSdk_t* sdk, unsigned long long seq, unsigned int limit, const char *proxy,const char* passwd,int timeout,Slice_t* chatDatas);
proxy | 否 | 使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081.如不使用代理可以设置为空. 支持sock5跟http代理 |
paswd | 否 | 代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123 |
在这里proxy是指我可以访问公网并且添加了对应白名单的机器,paswd是指用户名和密码,中间用【:】分隔。
首先需要一台代理机器,开始我再代理机器A上部署了nginx,并且用curl也可以尝试请求到数据
curl --proxy http://192.168.xx.xx:8001 http://qyapi.weixin.qq.com/cgi-bin/gettoken
但是curl https的时候会返回400
curl --proxy https://192.168.xx.xx:8001 https://qyapi.weixin.qq.com/cgi-bin/gettoken
这里前面是对应A机器的内网地址,8001是转发服务的端口,nginx的配置也只是利用8001的端口进行监听,然后转发到qyapi.weixin.qq.com,后来在sdk包联调调用的时候,想到转发的接口用到的应该是https请求,但是https在ng中需要升级对应的模块,而且ng上配置也比较多,不能随便乱改,后台直接在A机器上装了squid代理服务。解决问题。
至于ng为什么转发https需要单独的模块,可以参考下面几篇文章
Nginx正向代理请求https报400_ibigboy-优快云博客
使用 Nginx 搭建 HTTPS 正向代理服务 | Bigzuo's Blog
linux 搭建squid可以参考如下文章,一种是源码安装,一种是yum安装
https://blog.youkuaiyun.com/weixin_30363817/article/details/98212574
https://www.cnblogs.com/suntray/p/14452861.html
最后在调用 curl --proxy https://192.168.xx.xx:8001 https://qyapi.weixin.qq.com/cgi-bin/gettoken
成功
------------------------------------------------------------------------------------------------------------------------------
下面继续聊一聊企业微信会话内容开发过程中的解密问题
首先还是看官方文档给出的内容讲解
ChatDatas详解:
{"errcode":0,"errmsg":"ok","chatdata":[{"seq":196,"msgid":"CAQQ2fbb4QUY0On2rYSAgAMgip/yzgs=","publickey_ver":3,"encrypt_random_key":"ftJ+uz3n/z1DsxlkwxNgE+mL38H42/KCvN8T60gbbtPD+Rta1hKTuQPzUzO6Hzne97MgKs7FfdDxDck/v8cDT6gUVjA2tZ/M7euSD0L66opJ/IUeBtpAtvgVSD5qhlaQjvfKJc/zPMGNK2xCLFYqwmQBZXbNT7uA69Fflm512nZKW/piK2RKdYJhRyvQnA1ISxK097sp9WlEgDg250fM5tgwMjujdzr7ehK6gtVBUFldNSJS7ndtIf6aSBfaLktZgwHZ57ONewWq8GJe7WwQf1hwcDbCh7YMG8nsweEwhDfUz+u8rz9an+0lgrYMZFRHnmzjgmLwrR7B/32Qxqd79A==","encrypt_chat_msg":"898WSfGMnIeytTsea7Rc0WsOocs0bIAerF6de0v2cFwqo9uOxrW9wYe5rCjCHHH5bDrNvLxBE/xOoFfcwOTYX0HQxTJaH0ES9OHDZ61p8gcbfGdJKnq2UU4tAEgGb8H+Q9n8syRXIjaI3KuVCqGIi4QGHFmxWenPFfjF/vRuPd0EpzUNwmqfUxLBWLpGhv+dLnqiEOBW41Zdc0OO0St6E+JeIeHlRZAR+E13Isv9eS09xNbF0qQXWIyNUi+ucLr5VuZnPGXBrSfvwX8f0QebTwpy1tT2zvQiMM2MBugKH6NuMzzuvEsXeD+6+3VRqL"}]}
返回参数说明:
参数 | 说明 |
---|---|
errcode | 0表示成功,错误返回非0错误码,需要参看errmsg。Uint32类型 |
errmsg | 返回信息,如非空为错误原因。String类型 |
chatdata | 聊天记录数据内容。数组类型。包括seq、msgid等内容 |
seq | 消息的seq值,标识消息的序号。再次拉取需要带上上次回包中最大的seq。Uint64类型,范围0-pow(2,64)-1 |
msgid | 消息id,消息的唯一标识,企业可以使用此字段进行消息去重。String类型。msgid以_external结尾的消息,表明该消息是一条外部消息。 |
publickey_ver | 加密此条消息使用的公钥版本号。Uint32类型 |
encrypt_random_key | 使用publickey_ver指定版本的公钥进行非对称加密后base64加密的内容,需要业务方先base64 decode处理后,再使用指定版本的私钥进行解密,得出内容。String类型 |
encrypt_chat_msg | 消息密文。需要业务方使用将encrypt_random_key解密得到的内容,与encrypt_chat_msg,传入sdk接口DecryptData,得到消息明文。String类型 |
encrypt_random_key内容解密说明:
encrypt_random_key是使用企业在管理端填写的公钥(使用模值为2048bit的秘钥),采用RSA加密算法进行加密处理后base64 encode的内容,加密内容为企业微信产生。RSA使用PKCS1。
企业通过GetChatData获取到会话数据后:
a) 需首先对每条消息的encrypt_random_key内容进行base64 decode,得到字符串str1.
b) 使用publickey_ver指定版本的私钥,使用RSA PKCS1算法对str1进行解密,得到解密内容str2.
c) 得到str2与对应消息的encrypt_chat_msg,调用下方描述的DecryptData接口,即可获得消息明文。
这里先聊一聊后台怎么配置,在配置秘钥的时候,先要生成好公钥和私钥,如果想方便一些,可以到下面的网站直接生成在线RSA公钥加密解密、RSA public key encryption and decryption--查错网
把上面的公钥内容设置到后台的公钥中,下面的私钥自己保存好,用于解密
然后解密的内容可以参考下面几篇文章
企业微信会话存档消息解密(Java RSA PKCS1解密) - 是谁啊? - 博客园
这里主要说一下PKCS8的秘钥格式,由于我当时生成秘钥的时候没有选秘钥格式,默认的是PKCS8,后来在同事的帮助下最终还是解析出来了数据内容。
记得调试的时候把
-----BEGIN PUBLIC KEY-----
-----END PUBLIC KEY-----
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
-----BEGIN RSA PRIVATE KEY-----
-----END RSA PRIVATE KEY-----
换行符/n都替换成""空字符串。
//encryptionDto是解析出来的密文对象
String encryptChatMsg = encryptionDto.getEncrypt_chat_msg();
String encryptRandomKey = encryptionDto.getEncrypt_random_key();
String encrypt_key = null;
try {
//syncSessionUtil.getPrivateKey()是自己保存的对应的私钥
encrypt_key = RSAUtil.getPrivateKey3(encryptRandomKey,syncSessionUtil.getPrivateKey());
} catch (Exception e) {
log.error("解密数据会话数据失败 encryptionDto:"+JSONObject.toJSONString(encryptionDto), e);
continue;
}
// 将获取到的数据进行解密操作
//long msg = Finance.NewSlice();
log.info("=======msgs==msgs=======:"+slice);
int ret = Finance.DecryptData(sdk, encrypt_key, encryptChatMsg, slice);
log.info("=======ret==ret=======:"+ret);
String decrypt_msg = Finance.GetContentFromSlice(slice);// 解密后的消息
log.info("解密后的内容 {}", decrypt_msg);
import org.apache.commons.codec.binary.Base64;
import javax.crypto.Cipher;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.HashMap;
import java.util.Map;
/**
* RSA加密和解密工具
*
*/
public class RSAUtil {
/**
* 数字签名,密钥算法
*/
private static final String RSA_KEY_ALGORITHM = "RSA";
/**
* 数字签名签名/验证算法
*/
private static final String SIGNATURE_ALGORITHM = "MD5withRSA";
/**
* RSA密钥长度,RSA算法的默认密钥长度是1024密钥长度必须是64的倍数,在512到65536位之间
*/
private static final int KEY_SIZE = 1024;
/**
* 生成密钥对
*/
private static Map<String, String> initKey() throws Exception {
KeyPairGenerator keygen = KeyPairGenerator.getInstance(RSA_KEY_ALGORITHM);
SecureRandom secrand = new SecureRandom();
/**
* 初始化随机产生器
*/
secrand.setSeed("initSeed".getBytes());
/**
* 初始化密钥生成器
*/
keygen.initialize(KEY_SIZE, secrand);
KeyPair keys = keygen.genKeyPair();
byte[] pub_key = keys.getPublic().getEncoded();
String publicKeyString = Base64.encodeBase64String(pub_key);
byte[] pri_key = keys.getPrivate().getEncoded();
String privateKeyString = Base64.encodeBase64String(pri_key);
Map<String, String> keyPairMap = new HashMap<String, String>();
keyPairMap.put("publicKeyString", publicKeyString);
keyPairMap.put("privateKeyString", privateKeyString);
return keyPairMap;
}
/**
* 密钥转成字符串
*
* @param key
* @return
*/
public static String encodeBase64String(byte[] key) {
return Base64.encodeBase64String(key);
}
/**
* 密钥转成byte[]
*
* @param key
* @return
*/
public static byte[] decodeBase64(String key) {
return Base64.decodeBase64(key);
}
/**
* 公钥加密
*
* @param data 加密前的字符串
* @param publicKey 公钥
* @return 加密后的字符串
* @throws Exception
*/
public static String encryptByPubKey(String data, String publicKey) throws Exception {
byte[] pubKey = RSAUtil.decodeBase64(publicKey);
byte[] enSign = encryptByPubKey(data.getBytes(), pubKey);
return Base64.encodeBase64String(enSign);
}
/**
* 公钥加密
*
* @param data 待加密数据
* @param pubKey 公钥
* @return
* @throws Exception
*/
public static byte[] encryptByPubKey(byte[] data, byte[] pubKey) throws Exception {
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
* 私钥加密
*
* @param data 加密前的字符串
* @param privateKey 私钥
* @return 加密后的字符串
* @throws Exception
*/
public static String encryptByPriKey(String data, String privateKey) throws Exception {
byte[] priKey = RSAUtil.decodeBase64(privateKey);
byte[] enSign = encryptByPriKey(data.getBytes(), priKey);
return Base64.encodeBase64String(enSign);
}
/**
* 私钥加密
*
* @param data 待加密的数据
* @param priKey 私钥
* @return 加密后的数据
* @throws Exception
*/
public static byte[] encryptByPriKey(byte[] data, byte[] priKey) throws Exception {
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
return cipher.doFinal(data);
}
/**
* 公钥解密
*
* @param data 待解密的数据
* @param pubKey 公钥
* @return 解密后的数据
* @throws Exception
*/
public static byte[] decryptByPubKey(byte[] data, byte[] pubKey) throws Exception {
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
/**
* 公钥解密
*
* @param data 解密前的字符串
* @param publicKey 公钥
* @return 解密后的字符串
* @throws Exception
*/
public static String decryptByPubKey(String data, String publicKey) throws Exception {
byte[] pubKey = RSAUtil.decodeBase64(publicKey);
byte[] design = decryptByPubKey(Base64.decodeBase64(data), pubKey);
return new String(design);
}
/*private static PrivateKey getPrivateKey (String privateKey) throws Exception {
Reader privateKeyReader = new StringReader(privateKey);
PEMParser privatePemParser = new PEMParser(privateKeyReader);
Object privateObject = privatePemParser.readObject();
if (privateObject instanceof PEMKeyPair) {
PEMKeyPair pemKeyPair = (PEMKeyPair) privateObject;
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
PrivateKey privKey = converter.getPrivateKey(pemKeyPair.getPrivateKeyInfo());
return privKey;
}
return null;
}*/
// //读取pkcs1格式的private key
// public static PrivateKey getPrivateKey2(String privKeyPEM) throws Exception{
//
// String privKeyPEMnew = privKeyPEM.replaceAll("\\n", "").replace("-----BEGIN RSA PRIVATE KEY-----", "").replace("-----END RSA PRIVATE KEY-----", "");
// byte[] bytes = RSAUtil.decodeBase64(privKeyPEMnew);
//
// DerInputStream derReader = new DerInputStream(bytes);
// DerValue[] seq = derReader.getSequence(0);
// 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");
// PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
// return privateKey;
// }
/*public static RSAPrivateKey loadPrivateKeyByStr(String privateKeyStr)
throws Exception {
try {
byte[] buffer = Base64.decodeBase64(privateKeyStr);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(buffer);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return (RSAPrivateKey) keyFactory.generatePrivate(keySpec);
} catch (NoSuchAlgorithmException e) {
throw new Exception("无此算法");
} catch (InvalidKeySpecException e) {
throw new Exception("私钥非法");
} catch (NullPointerException e) {
throw new Exception("私钥数据为空");
}
}*/
/**
* 私钥解密
*
* @param data 待解密的数据
* @param priKey 私钥
* @return
* @throws Exception
*/
/*public static byte[] decryptByPriKey(byte[] data, byte[] priKey) throws Exception {
// PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
// KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
RSAPrivateKeyStructure asn1PrivKey = new RSAPrivateKeyStructure((ASN1Sequence) ASN1Sequence.fromByteArray(priKey));
RSAPrivateKeySpec rsaPrivKeySpec = new RSAPrivateKeySpec(asn1PrivKey.getModulus(), asn1PrivKey.getPrivateExponent());
KeyFactory keyFactory= KeyFactory.getInstance("RSA");
PrivateKey privateKey= keyFactory.generatePrivate(rsaPrivKeySpec);
Cipher cipher = Cipher.getInstance(keyFactory.getAlgorithm());
cipher.init(Cipher.DECRYPT_MODE, privateKey);
return cipher.doFinal(data);
}*/
/**
* 私钥解密
*
* @param data 解密前的字符串
* @param privateKey 私钥
* @return 解密后的字符串
* @throws Exception
*/
/*public static String decryptByPriKey(String data, String privateKey) throws Exception {
Security.addProvider(new BouncyCastleProvider());
Cipher rsa = Cipher.getInstance("RSA/ECB/PKCS1Padding", "BC");
rsa.init(Cipher.DECRYPT_MODE, getPrivateKey(privateKey));
byte[] utf8 = rsa.doFinal(Base64.decodeBase64(data));
String result = new String(utf8,"UTF-8");
return result;
}*/
/**
* RSA签名
*
* @param data 待签名数据
* @param priKey 私钥
* @return 签名
* @throws Exception
*/
public static String sign(byte[] data, byte[] priKey) throws Exception {
// 取得私钥
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(priKey);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// 生成私钥
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
// 实例化Signature
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
// 初始化Signature
signature.initSign(privateKey);
// 更新
signature.update(data);
return Base64.encodeBase64String(signature.sign());
}
/**
* RSA校验数字签名
*
* @param data 待校验数据
* @param sign 数字签名
* @param pubKey 公钥
* @return boolean 校验成功返回true,失败返回false
*/
public boolean verify(byte[] data, byte[] sign, byte[] pubKey) throws Exception {
// 实例化密钥工厂
KeyFactory keyFactory = KeyFactory.getInstance(RSA_KEY_ALGORITHM);
// 初始化公钥
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(pubKey);
// 产生公钥
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
// 实例化Signature
Signature signature = Signature.getInstance(SIGNATURE_ALGORITHM);
// 初始化Signature
signature.initVerify(publicKey);
// 更新
signature.update(data);
// 验证
return signature.verify(sign);
}
// 用此方法先获取秘钥
public static String getPrivateKey3(String str, String privKeyPEMnew) throws Exception {
// String privKeyPEMnew = privKeyPEM.replaceAll("\\n", "").replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "");
byte[] decoded = java.util.Base64.getDecoder().decode(privKeyPEMnew);
java.security.interfaces.RSAPrivateKey priKey = (java.security.interfaces.RSAPrivateKey) KeyFactory.getInstance("RSA")
.generatePrivate(new PKCS8EncodedKeySpec(decoded));
// 64位解码加密后的字符串
byte[] inputByte = java.util.Base64.getDecoder().decode(str);
// RSA解密
Cipher cipher = Cipher.getInstance("RSA");
cipher.init(Cipher.DECRYPT_MODE, priKey);
String outStr = new String(cipher.doFinal(inputByte));
return outStr;
}
}