wechat_crypto/
lib.rs

1//! 解决企业微信数据解码解密时遇到的异常问题,以便正常解析内容
2//!
3//! 可以应用在以下场景
4//!
5//! * 企业微信回调接口签名验证和解密
6//! * 企业微信通讯录导出数据解密
7//!
8//! ## Example
9//! ```rust
10//! use base64::Engine;
11//! use base64::engine::general_purpose::STANDARD;
12//! use wechat_crypto::{calc_signature, decode_aes_key, decrypt, parse_plain_text};
13//!
14//! let encoded_aes_key = "kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aQ";
15//! // 解码 aes_key
16//! let aes_key = decode_aes_key(encoded_aes_key).unwrap();
17//!
18//! // 解密数据收到的数据
19//! let r = decrypt(
20//!     &aes_key,
21//!     &STANDARD
22//!         .decode("9s4gMv99m88kKTh/H8IdkNiFGeG9pd7vNWl50fGRWXY=")
23//!         .unwrap(),
24//! )
25//! .unwrap();
26//! dbg!(String::from_utf8_lossy(&r).to_string());
27//!
28//! // 提取数据中的正文和 receiver_id
29//! let (t, r) = parse_plain_text(&r).unwrap();
30//! assert_eq!("test", &t);
31//!
32//! // 签名验证
33//! let token = "QDG6eK";
34//! let verify_msg_sign = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3";
35//! let verify_timestamp = "1409659589";
36//! let verify_nonce = "263014780";
37//! let verify_echo_str = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
38//!
39//! // 验证签名是否匹配
40//! assert_eq!(
41//!     verify_msg_sign,
42//!     calc_signature(token, verify_timestamp, verify_nonce, verify_echo_str)
43//! );
44//!
45//! ```
46use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
47use anyhow::{anyhow, Result};
48use base64::alphabet::STANDARD;
49use base64::engine::{GeneralPurpose, GeneralPurposeConfig};
50use base64::Engine;
51use byteorder::{BigEndian, WriteBytesExt};
52use cbc::cipher::block_padding::NoPadding;
53use serde::{Deserialize, Serialize};
54use sha1::{Digest, Sha1};
55use std::iter::repeat_with;
56
57/// 验证签名的必须参数,该参数从 URL 获取
58#[derive(Deserialize, Serialize, Debug)]
59pub struct VerifyInfo {
60    /// 企业微信签名,msg_signature
61    #[serde(rename = "msg_signature")]
62    pub signature: String,
63    /// 时间戳 timestamp
64    pub timestamp: i64,
65    /// 随机数
66    pub nonce: i64,
67}
68/// 解决企业微信 base64 数据 padding 问题
69const G: GeneralPurpose = GeneralPurpose::new(
70    &STANDARD,
71    GeneralPurposeConfig::new().with_decode_allow_trailing_bits(true),
72);
73
74/// 对原始的 encoded_aes_key 进行解码
75pub fn decode_aes_key(encoded_aes_key: &str) -> Result<Vec<u8>> {
76    Ok(G.decode(format!("{}=", encoded_aes_key))?)
77}
78
79/// 计算签名函数
80/// ```rust
81/// use base64::Engine;
82/// use base64::engine::general_purpose::STANDARD;
83/// use wechat_crypto::{calc_signature, decode_aes_key, decrypt, parse_plain_text};
84/// fn test_calc_signature() {
85///     let token = "QDG6eK";
86///     let receiver_id = "wx5823bf96d3bd56c7";
87///     let encoded_aes_key = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
88///     let verify_msg_sign = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3";
89///     let verify_timestamp = "1409659589";
90///     let verify_nonce = "263014780";
91///     let verify_echo_str = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
92///     assert_eq!(
93///         verify_msg_sign,
94///         calc_signature(token, verify_timestamp, verify_nonce, verify_echo_str)
95///     );
96///     let v = STANDARD
97///         .decode(verify_echo_str)
98///         .unwrap();
99///     let aes_key = decode_aes_key(encoded_aes_key).unwrap();
100///     let r = decrypt(aes_key.as_slice(), v.as_slice()).unwrap();
101///     let (m, r) = dbg!(parse_plain_text(&r).unwrap());
102///     assert_eq!(r, receiver_id);
103///     assert_eq!("1616140317555161061", m.as_str());
104/// }
105/// ```
106pub fn calc_signature(token: &str, ts: &str, nonce: &str, data: &str) -> String {
107    let mut sort_arr = vec![token, ts, nonce, data];
108    sort_arr.sort();
109    let mut buffer = String::new();
110    for value in sort_arr {
111        buffer.push_str(value);
112    }
113
114    let mut sha = Sha1::new();
115
116    sha.update(buffer.as_bytes());
117    let signature = sha.finalize();
118    format!("{:x}", signature)
119}
120
121/// 企业微信回调接口验证逻辑
122///
123/// 请根据使用的 http 框架获取 url 参数,然后传入该函数,该函数使用本 crate 其他几个函数组合完成签名验证。
124///
125/// 该函数未验证时间戳区间,需要自行验证
126/// ```rust
127/// use base64::Engine;
128/// use base64::engine::general_purpose::STANDARD;
129/// use wechat_crypto::{calc_signature, decode_aes_key, decrypt, parse_plain_text, verify_url, VerifyInfo};
130/// fn test_verify_url() -> anyhow::Result<()> {
131///     let token = "QDG6eK";
132///     let receiver_id = "wx5823bf96d3bd56c7";
133///     let encoded_aes_key = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
134///     let aes_key = decode_aes_key(encoded_aes_key).unwrap();
135///
136///     let verify_msg_sign = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3";
137///     let verify_timestamp = 1409659589;
138///     let verify_nonce = 263014780;
139///     let verify_echo_str = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
140///
141///     let echo_str = verify_url(
142///         token,
143///         &VerifyInfo {
144///             signature: verify_msg_sign.to_string(),
145///             timestamp: verify_timestamp,
146///             nonce: verify_nonce,
147///         },
148///         verify_echo_str,
149///         aes_key.as_slice(),
150///         receiver_id,
151///     )?;
152///     assert_eq!("1616140317555161061", echo_str.as_str());
153///     Ok(())
154/// }
155/// ```
156pub fn verify_url(
157    token: &str,
158    q: &VerifyInfo,
159    echo_str: &str,
160    aes_key: &[u8],
161    corp_id: &str,
162) -> Result<String> {
163    let signature = calc_signature(
164        token,
165        q.timestamp.to_string().as_str(),
166        q.nonce.to_string().as_str(),
167        echo_str,
168    );
169    if signature != q.signature {
170        return Err(anyhow::anyhow!("签名不正确"));
171    }
172    let es = base64::engine::general_purpose::STANDARD
173        .decode(echo_str)
174        .map_err(|e| anyhow::Error::new(e).context("echo_str base64 解密失败"))?;
175    let plaintext = decrypt(aes_key, &es)?;
176    let (msg, receiver_id) = parse_plain_text(&plaintext)?;
177    if receiver_id != corp_id {
178        return Err(anyhow!("receiver_id={} 与服务端配置不一致", receiver_id));
179    }
180    Ok(msg)
181}
182
183/// 对解密后的数据进行还原
184///
185/// 移除前16位随机数,返回消息体和消息的 receiver_id
186pub fn parse_plain_text(plaintext: &[u8]) -> Result<(String, String)> {
187    // let random = &plaintext[..16];
188    let msg_len = u32::from_be_bytes([plaintext[16], plaintext[17], plaintext[18], plaintext[19]]);
189    let msg = &plaintext[20..(20 + msg_len as usize)];
190    let receiver_id = &plaintext[(20 + msg_len as usize)..];
191    Ok((
192        String::from_utf8_lossy(msg).to_string(),
193        String::from_utf8_lossy(receiver_id).to_string(),
194    ))
195}
196
197type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
198type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
199/// 使用 AES256 CBC 解密,解决了 PKCS7 填充问题
200/// ```rust
201/// use wechat_crypto::{decode_aes_key, decrypt, parse_plain_text};
202/// use base64::Engine;
203/// use base64::engine::general_purpose::STANDARD;
204/// fn test_decrypt() {
205///     let encoded_aes_key = "kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aQ";
206///     let aes_key = decode_aes_key(encoded_aes_key).unwrap();
207///     let r = decrypt(
208///         aes_key.as_slice(),
209///         &STANDARD
210///             .decode("9s4gMv99m88kKTh/H8IdkNiFGeG9pd7vNWl50fGRWXY=")
211///             .unwrap(),
212///     )
213///     .unwrap();
214///     dbg!(String::from_utf8(r.clone()).unwrap());
215///     let (t, _) = parse_plain_text(&r).unwrap();
216///     assert_eq!("test", &t);
217/// }
218/// ```
219pub fn decrypt(aes_key: &[u8], data: &[u8]) -> Result<Vec<u8>> {
220    let iv = &aes_key[..16];
221    let key = &aes_key[..32];
222
223    let cipher = Aes256CbcDec::new_from_slices(key, iv)
224        .map_err(|e| anyhow::Error::new(e).context("初始化解密函数失败"))?;
225    let mut buffer = vec![0u8; data.len()];
226
227    let r = cipher
228        .decrypt_padded_b2b_mut::<NoPadding>(data, &mut buffer)
229        .map_err(|e| anyhow!("解密失败 {}", e))?;
230    let end = r.len() - (r[r.len() - 1] as usize);
231    Ok(r[..end].to_vec())
232}
233
234/// 使用 AES256 CBC 按照微信文档数据格式进行加密
235/// ```rust
236/// use base64::Engine;
237/// use base64::engine::general_purpose::STANDARD;
238/// use wechat_crypto::{decode_aes_key, encrypt};
239/// fn test_encrypt() -> anyhow::Result<()> {
240///     let encoded_aes_key = "kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aQ";
241///     let aes_key = decode_aes_key(encoded_aes_key)?;
242///     let encrypted = encrypt(aes_key.as_slice(), "test", "rust").unwrap();
243///     assert_eq!(
244///         "9s4gMv99m88kKTh/H8IdkNiFGeG9pd7vNWl50fGRWXY=",
245///         &STANDARD.encode(encrypted)
246///     );
247///     Ok(())
248/// }
249/// ```
250pub fn encrypt(aes_key: &[u8], plaintext: &str, corp_id: &str) -> Result<Vec<u8>> {
251    let mut wtr = gen_random_byte();
252    wtr.write_u32::<BigEndian>(plaintext.len() as u32)
253        .map_err(|e| anyhow::Error::new(e).context("写入数据长度失败"))?;
254    wtr.extend(plaintext.bytes());
255    wtr.extend(corp_id.bytes());
256
257    let iv = &aes_key[..16];
258    let key = &aes_key[..32];
259
260    let cipher = Aes256CbcEnc::new_from_slices(key, iv)
261        .map_err(|e| anyhow::Error::new(e).context("初始化加密函数失败"))?;
262
263    let mut buffer = vec![0u8; (wtr.len() + 15) / 16 * 16];
264    let r = cipher
265        .encrypt_padded_b2b_mut::<Pkcs7>(wtr.as_slice(), &mut buffer)
266        .map_err(|e| anyhow!("解密失败 {}", e))?;
267    Ok(r.to_vec())
268}
269
270fn gen_random_byte() -> Vec<u8> {
271    if cfg!(test) {
272        vec![
273            49u8, 50, 51, 52, 53, 54, 55, 56, 57, 48, 49, 50, 51, 52, 53, 54,
274        ]
275    } else {
276        repeat_with(|| fastrand::u8(..)).take(16).collect()
277    }
278}
279#[cfg(test)]
280mod test {
281    use super::*;
282
283    use base64::Engine;
284
285    #[test]
286    fn test_calc_signature() {
287        let token = "QDG6eK";
288        let receiver_id = "wx5823bf96d3bd56c7";
289        let encoded_aes_key = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
290        let verify_msg_sign = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3";
291        let verify_timestamp = "1409659589";
292        let verify_nonce = "263014780";
293        let verify_echo_str = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
294        // 	echoStr, cryptErr := wxcpt.VerifyURL(verify_msg_sign, verify_timestamp, verify_nonce, verify_echo_str)
295        assert_eq!(
296            verify_msg_sign,
297            calc_signature(token, verify_timestamp, verify_nonce, verify_echo_str)
298        );
299        let v = base64::engine::general_purpose::STANDARD
300            .decode(verify_echo_str)
301            .unwrap();
302        let aes_key = decode_aes_key(encoded_aes_key).unwrap();
303        let r = decrypt(aes_key.as_slice(), v.as_slice()).unwrap();
304        let (m, r) = dbg!(parse_plain_text(&r).unwrap());
305        assert_eq!(r, receiver_id);
306        assert_eq!("1616140317555161061", m.as_str());
307
308        let token = "QDG6eK";
309        let signature = "477715d11cdb4164915debcba66cb864d751f3e6";
310        let timestamps = "1409659813";
311        let nonce = "1372623149";
312        let msg_encrypt = "RypEvHKD8QQKFhvQ6QleEB4J58tiPdvo+rtK1I9qca6aM/wvqnLSV5zEPeusUiX5L5X/0lWfrf0QADHHhGd3QczcdCUpj911L3vg3W/sYYvuJTs3TUUkSUXxaccAS0qhxchrRYt66wiSpGLYL42aM6A8dTT+6k4aSknmPj48kzJs8qLjvd4Xgpue06DOdnLxAUHzM6+kDZ+HMZfJYuR+LtwGc2hgf5gsijff0ekUNXZiqATP7PF5mZxZ3Izoun1s4zG4LUMnvw2r+KqCKIw+3IQH03v+BCA9nMELNqbSf6tiWSrXJB3LAVGUcallcrw8V2t9EL4EhzJWrQUax5wLVMNS0+rUPA3k22Ncx4XXZS9o0MBH27Bo6BpNelZpS+/uh9KsNlY6bHCmJU9p8g7m3fVKn28H3KDYA5Pl/T8Z1ptDAVe0lXdQ2YoyyH2uyPIGHBZZIs2pDBS8R07+qN+E7Q==";
313
314        assert_eq!(
315            signature,
316            calc_signature(token, timestamps, nonce, msg_encrypt)
317        );
318
319        let v = base64::engine::general_purpose::STANDARD
320            .decode(msg_encrypt)
321            .unwrap();
322
323        dbg!(parse_plain_text(&decrypt(aes_key.as_slice(), v.as_slice()).unwrap()).unwrap());
324        let signature = calc_signature("test", "123456", "test", "rust");
325        assert_eq!("d6056f2bb3ad3e30f4afa5ef90cc9ddcdc7b7b27", signature);
326
327        let receiver_id = "wx49f0ab532d5d035a";
328        let encoded_aes_key = "kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aQ";
329        let verify_echo_str = "4ByGGj+sVCYcvGeQYhaKIk1o0pQRNbRjxybjTGblXrBaXlTXeOo1+bXFXDQQb1o6co6Yh9Bv41n7hOchLF6p+Q==";
330
331        let v = base64::engine::general_purpose::STANDARD
332            .decode(verify_echo_str)
333            .unwrap();
334        let aes_key = decode_aes_key(encoded_aes_key).unwrap();
335        let r = decrypt(aes_key.as_slice(), v.as_slice()).unwrap();
336        let (m, r) = dbg!(parse_plain_text(&r).unwrap());
337        assert_eq!(r, receiver_id);
338        assert_eq!("5927782489442352469", m.as_str());
339    }
340
341    #[test]
342    fn test_decode_aes_key() -> Result<()> {
343        let encoded_aes_key = "IJUiXNpvGbODwKEBSEsAeOAPAhkqHqNCF6g19t9wfg2";
344        let b = decode_aes_key(encoded_aes_key)?;
345        let a = [
346            32u8, 149, 34, 92, 218, 111, 25, 179, 131, 192, 161, 1, 72, 75, 0, 120, 224, 15, 2, 25,
347            42, 30, 163, 66, 23, 168, 53, 246, 223, 112, 126, 13,
348        ];
349        assert_eq!(a, b.as_slice());
350        Ok(())
351    }
352
353    #[test]
354    fn test_decrypt() {
355        let encoded_aes_key = "kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aQ";
356        let aes_key = decode_aes_key(encoded_aes_key).unwrap();
357        let r = decrypt(
358            aes_key.as_slice(),
359            &base64::engine::general_purpose::STANDARD
360                .decode("9s4gMv99m88kKTh/H8IdkNiFGeG9pd7vNWl50fGRWXY=")
361                .unwrap(),
362        )
363        .unwrap();
364        dbg!(String::from_utf8(r.clone()).unwrap());
365        let (t, _) = parse_plain_text(&r).unwrap();
366        assert_eq!("test", &t);
367    }
368
369    #[test]
370    fn test_encrypt() -> Result<()> {
371        let encoded_aes_key = "kWxPEV2UEDyxWpmPdKC3F4dgPDmOvfKX1HGnEUDS1aQ";
372        let aes_key = decode_aes_key(encoded_aes_key)?;
373        let encrypted = encrypt(aes_key.as_slice(), "test", "rust").unwrap();
374        assert_eq!(
375            "9s4gMv99m88kKTh/H8IdkNiFGeG9pd7vNWl50fGRWXY=",
376            &base64::engine::general_purpose::STANDARD.encode(encrypted)
377        );
378        Ok(())
379    }
380
381    #[test]
382    fn test_verify_url() -> Result<()> {
383        let token = "QDG6eK";
384        let receiver_id = "wx5823bf96d3bd56c7";
385        let encoded_aes_key = "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C";
386        let aes_key = decode_aes_key(encoded_aes_key).unwrap();
387
388        let verify_msg_sign = "5c45ff5e21c57e6ad56bac8758b79b1d9ac89fd3";
389        let verify_timestamp = 1409659589;
390        let verify_nonce = 263014780;
391        let verify_echo_str = "P9nAzCzyDtyTWESHep1vC5X9xho/qYX3Zpb4yKa9SKld1DsH3Iyt3tP3zNdtp+4RPcs8TgAE7OaBO+FZXvnaqQ==";
392
393        let echo_str = verify_url(
394            token,
395            &VerifyInfo {
396                signature: verify_msg_sign.to_string(),
397                timestamp: verify_timestamp,
398                nonce: verify_nonce,
399            },
400            verify_echo_str,
401            aes_key.as_slice(),
402            receiver_id,
403        )?;
404        assert_eq!("1616140317555161061", echo_str.as_str());
405        Ok(())
406    }
407}