Files
JoyD/WeCom/src/main.rs
zqm 9e81bed829 API接收消息
接收消息服务器配置
URL
https://pactgo.cn/wecom
Token
mytoken123456
EncodingAESKey
PXP7FjoinIPc9WscGymDlf1VwMyBLh1cKJJSJFx2SO8
接收的消息事件类型
用户发送的普通消息,自定义菜单操作,上报地理位置 ,上报进入应用事件,审批状态通知事件,外部联系人变更回调,微信客服消息和事件,支付和退款通知,上下游变更回调,直播状态变更回调,会议室预定状态变更回调,成员申请高级功能状态通知
2026-03-12 10:46:39 +08:00

157 lines
4.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use aes::Aes256;
use aes::cipher::{BlockDecrypt, KeyInit};
use aes::cipher::generic_array::GenericArray;
use base64::{engine::general_purpose::{STANDARD, URL_SAFE}, Engine};
use urlencoding;
use axum::{extract::Query, routing::get, Router};
use serde::Deserialize;
use sha1_smol::Sha1;
#[derive(Debug, Deserialize)]
pub struct WecomVerify {
msg_signature: String,
timestamp: String,
nonce: String,
echostr: String,
}
const WECOM_TOKEN: &str = "mytoken123456";
const CORP_ID: &str = "wwa7bb7aec981103b4";
const ENCODING_AES_KEY: &str = "PXP7FjoinIPc9WscGymDlf1VwMyBLh1cKJJSJFx2SO8";
fn url_decode(s: &str) -> String {
match urlencoding::decode(s) {
Ok(decoded) => decoded.into_owned(),
Err(_) => s.to_string(),
}
}
fn decrypt_aes_cbc(key: &[u8; 32], iv: &[u8; 16], ciphertext: &[u8]) -> Vec<u8> {
let cipher = Aes256::new(key.into());
let mut plaintext = Vec::with_capacity(ciphertext.len());
let mut prev_iv = *iv;
for block in ciphertext.chunks_exact(16) {
let mut block_arr = GenericArray::clone_from_slice(block);
cipher.decrypt_block(&mut block_arr);
// CBC 解密:异或 IV/前一个密文块
for i in 0..16 {
block_arr[i] ^= prev_iv[i];
}
plaintext.extend_from_slice(&block_arr);
prev_iv.copy_from_slice(block);
}
// 企业微信 echostr 特殊处理:强制去除 PKCS7 填充
// 因为 echostr 是随机串,解密后填充可能无效,但仍需去除
let len = plaintext.len();
if len == 0 { return plaintext; }
let pad = plaintext[len - 1] as usize;
if pad >= 1 && pad <= 16 && len >= pad {
// 强制去除填充,不验证有效性
plaintext.truncate(len - pad);
}
plaintext
}
fn decrypt_echo_str(encrypted: &str) -> Result<String, String> {
// 企业微信官方要求EncodingAESKey 需要在末尾添加 "=" 再解码
let key = URL_SAFE.decode(format!("{}=", ENCODING_AES_KEY)).map_err(|e| e.to_string())?;
// 企业微信 URL 验证echostr使用标准 Base64 解码
let ciphertext = STANDARD.decode(encrypted).map_err(|e| e.to_string())?;
if key.len() != 32 {
return Err(format!("Invalid key length: {}", key.len()));
}
let mut key_array = [0u8; 32];
key_array.copy_from_slice(&key);
let iv = &key_array[0..16];
let mut iv_array = [0u8; 16];
iv_array.copy_from_slice(iv);
let plaintext = decrypt_aes_cbc(&key_array, &iv_array, &ciphertext);
// 企业微信官方格式16字节随机串 + 4字节长度(网络序) + 消息内容 + CorpID
if plaintext.len() < 20 {
return Err("Decrypted data too short".into());
}
// 解析 4 字节长度(网络序,大端)
let msg_len = u32::from_be_bytes([plaintext[16], plaintext[17], plaintext[18], plaintext[19]]);
let msg_start = 20;
let msg_end = msg_start + msg_len as usize;
if msg_end > plaintext.len() {
return Err("Invalid message length".into());
}
// 截取正确的消息内容
let msg = &plaintext[msg_start..msg_end];
let result = String::from_utf8_lossy(msg).to_string();
eprintln!("✅ 最终解密结果: {}", result);
Ok(result)
}
fn verify_signature(msg_signature: &str, timestamp: &str, nonce: &str, echostr: &str) -> bool {
let mut arr = vec![WECOM_TOKEN, timestamp, nonce, echostr];
arr.sort();
let mut hasher = Sha1::new();
hasher.update(arr.join("").as_bytes());
let signature = hasher.digest().to_string();
signature == msg_signature
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/wecom", get(verify));
println!("✅ 服务已启动127.0.0.1:8000");
println!("Token: {}", WECOM_TOKEN);
println!("CorpID: {}", CORP_ID);
axum::serve(
tokio::net::TcpListener::bind("127.0.0.1:8000").await.unwrap(),
app
).await.unwrap();
}
async fn verify(Query(q): Query<WecomVerify>) -> String {
let timestamp = url_decode(&q.timestamp);
let nonce = url_decode(&q.nonce);
let echostr = url_decode(&q.echostr);
eprintln!("=== 收到企业微信验证请求 ===");
eprintln!("msg_signature: {}", q.msg_signature);
eprintln!("timestamp: {}", timestamp);
eprintln!("nonce: {}", nonce);
eprintln!("echostr: {}", echostr);
if !verify_signature(&q.msg_signature, &timestamp, &nonce, &echostr) {
eprintln!("签名验证失败");
return "invalid signature".to_string();
}
eprintln!("签名验证成功");
match decrypt_echo_str(&echostr) {
Ok(msg) => {
eprintln!("解密成功msg: {}", msg);
msg
},
Err(e) => {
eprintln!("解密失败: {}", e);
"decrypt error".to_string()
}
}
}