Files
JoyD/WeCom/src/main.rs

157 lines
4.8 KiB
Rust
Raw Normal View History

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};
2026-03-11 13:18:05 +08:00
use serde::Deserialize;
use sha1_smol::Sha1;
2026-03-11 13:18:05 +08:00
#[derive(Debug, Deserialize)]
2026-03-11 13:18:05 +08:00
pub struct WecomVerify {
msg_signature: String,
timestamp: String,
nonce: String,
echostr: String,
2026-03-11 13:18:05 +08:00
}
const WECOM_TOKEN: &str = "mytoken123456";
const CORP_ID: &str = "wwa7bb7aec981103b4";
const ENCODING_AES_KEY: &str = "PXP7FjoinIPc9WscGymDlf1VwMyBLh1cKJJSJFx2SO8";
2026-03-11 13:18:05 +08:00
fn url_decode(s: &str) -> String {
match urlencoding::decode(s) {
Ok(decoded) => decoded.into_owned(),
Err(_) => s.to_string(),
}
2026-03-11 13:18:05 +08:00
}
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];
2026-03-11 13:18:05 +08:00
arr.sort();
let mut hasher = Sha1::new();
hasher.update(arr.join("").as_bytes());
let signature = hasher.digest().to_string();
2026-03-11 13:18:05 +08:00
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()
}
2026-03-11 13:18:05 +08:00
}
}