wecom_crypto/
lib.rs

1//! # wecom-crypto
2//!
3//! `wecom-crypto`提供了企业微信API数据的加解密功能。其实现完全遵循官方文档中的规定。
4//!
5//! ## 使用方法
6//! ```
7//! use wecom_crypto::{Agent, Source};
8//!
9//! let token = "a";
10//! let key = "cGCVnNJRgRu6wDgo7gxG2diBovGnRQq1Tqy4Rm4V4qF";
11//! let agent = Agent::new(token, key);
12//! let source = Source {
13//!     text: "hello world!".to_string(),
14//!     receive_id: "wandering-ai".to_string(),
15//! };
16//! let enc = agent.encrypt(&source);
17//! let dec = agent.decrypt(enc.as_str()).unwrap();
18//! assert_eq!(source, dec);
19//! ```
20
21use aes::{
22    self,
23    cipher::{
24        block_padding::{NoPadding, Pkcs7},
25        BlockDecryptMut, BlockEncryptMut, KeyIvInit,
26    },
27    Aes256,
28};
29use base64::{alphabet, engine, Engine as _};
30use cbc::{Decryptor, Encryptor};
31use sha1::{Digest, Sha1};
32use thiserror::Error;
33
34/// 加解密过程中可能出现的错误
35#[derive(Error, Debug)]
36pub enum CryptoError {
37    #[error("Wecom decode error. {0}")]
38    WecomDecode(String),
39    #[error("Base64 decode error. {0}")]
40    Base64Decode(#[from] base64::DecodeError),
41    #[error("UTF-8 decode error. {0}")]
42    Utf8Decode(#[from] std::string::FromUtf8Error),
43    #[error("AES decryption error. {0}")]
44    AesDecryption(String),
45}
46
47/// 生成数据签名,用于校验请求数据是否被篡改。输入需要包含Token。
48pub fn generate_sha1_signature(inputs: &[&str]) -> String {
49    let mut content = inputs.to_vec();
50    content.sort_unstable();
51    let digest = Sha1::digest(content.concat().as_bytes());
52    base16ct::lower::encode_string(&digest)
53}
54
55/// 加解密数据结构体。
56#[derive(PartialEq, Debug)]
57pub struct Source {
58    /// 待加密(解密后)的消息。
59    pub text: String,
60    /// 在企业应用回调中为corpid;在第三方事件回调中为suiteid;在个人主体的第三方应用中为一个空字符串。
61    pub receive_id: String,
62}
63
64// AES解密常量化区块大小
65const AES_BLOCK_SIZE: usize = 32;
66
67/// 加解密功能代理。是加解密方法的数据结构载体。
68#[derive(Clone)]
69pub struct Agent {
70    token: String,
71    key: [u8; AES_BLOCK_SIZE],
72    nonce: [u8; 16],
73}
74
75impl Agent {
76    /// 使用给定的Token和AES加密Key初始化代理。参数`key`为BASE64编码后的字符串。
77    pub fn new(token: &str, key: &str) -> Self {
78        // The AES key is BASE64 encoded. Be careful this encoding key generated
79        // by Tencent is buggy.
80        let config = engine::GeneralPurposeConfig::new()
81            .with_encode_padding(false)
82            .with_decode_allow_trailing_bits(true)
83            .with_decode_padding_mode(engine::DecodePaddingMode::Indifferent);
84        let key_as_vec = engine::GeneralPurpose::new(&alphabet::STANDARD, config)
85            .decode(key)
86            .expect("AES key should be valid Base64 string");
87        let key =
88            <[u8; AES_BLOCK_SIZE]>::try_from(key_as_vec).expect("AES key length should be 32");
89        let nonce = <[u8; 16]>::try_from(&key[..16]).unwrap();
90        Self {
91            token: token.to_owned(),
92            key,
93            nonce,
94        }
95    }
96
97    /// 根据请求数据生成签名,用于校验微信服务器的请求是否合规。输入数据不需要包含Token。
98    /// # Example
99    /// ```
100    /// use wecom_crypto::Agent;
101    ///
102    /// let agent = Agent::new("a", "cGCVnNJRgRu6wDgo7gxG2diBovGnRQq1Tqy4Rm4V4qF");
103    /// assert_eq!(
104    ///     agent.generate_signature(&["0", "c", "b"]),
105    ///     "a8addbc99f8b3f51d2adbceb605d650b9a8940e2"
106    /// )
107    /// ```
108    pub fn generate_signature(&self, inputs: &[&str]) -> String {
109        let mut content = inputs.to_vec();
110        content.push(&self.token);
111        generate_sha1_signature(&content)
112    }
113
114    /// 加密给定的数据结构体。加密后的字符串为BASE64编码后的数据。
115    pub fn encrypt(&self, input: &Source) -> String {
116        // 待加密数据
117        let mut block: Vec<u8> = Vec::new();
118
119        // 16字节随机数据
120        block.extend(rand::random::<[u8; 16]>());
121
122        // 明文字符串长度
123        block.extend((input.text.len() as u32).to_be_bytes());
124
125        // 明文字符串
126        block.extend(input.text.as_bytes());
127
128        // Receive ID
129        block.extend(input.receive_id.as_bytes());
130
131        // 加密
132        let cipher_bytes = Encryptor::<Aes256>::new(&self.key.into(), &self.nonce.into())
133            .encrypt_padded_vec_mut::<Pkcs7>(&block);
134        engine::general_purpose::STANDARD.encode(cipher_bytes)
135    }
136
137    /// 解密BASE64编码的加密数据。解密后的数据为Source类型。
138    pub fn decrypt(&self, encoded: &str) -> Result<Source, CryptoError> {
139        // Base64解码
140        let cipher_bytes = engine::general_purpose::STANDARD.decode(encoded)?;
141        // AES解密
142        let block = Decryptor::<Aes256>::new(&self.key.into(), &self.nonce.into())
143            .decrypt_padded_vec_mut::<NoPadding>(&cipher_bytes)
144            .map_err(|e| CryptoError::AesDecryption(e.to_string()))?;
145        // 获取填充长度与消息长度
146        let Some(padding_size) = block.last().copied() else {
147            return Err(CryptoError::WecomDecode(
148                "Failed to get padding size, empty block".to_string(),
149            ));
150        };
151        let Ok(msg_len_bytes): Result<[u8; 4], _> = block[16..20].try_into() else {
152            return Err(CryptoError::WecomDecode("Invalid message size".to_string()));
153        };
154        // 提取消息
155        let msg_len = u32::from_be_bytes(msg_len_bytes) as usize;
156        let text = String::from_utf8(block[20..20 + msg_len].to_vec())?;
157        let receive_id =
158            String::from_utf8(block[20 + msg_len..block.len() - padding_size as usize].to_vec())?;
159        Ok(Source { text, receive_id })
160    }
161}
162
163#[cfg(test)]
164mod tests {
165    use super::*;
166    #[test]
167    fn test_signature() {
168        let token = "a";
169        let key = "cGCVnNJRgRu6wDgo7gxG2diBovGnRQq1Tqy4Rm4V4qF";
170        let agent = Agent::new(token, key);
171        assert_eq!(
172            agent.generate_signature(&["0", "c", "b"]),
173            "a8addbc99f8b3f51d2adbceb605d650b9a8940e2",
174        );
175    }
176
177    #[test]
178    fn test_mod_signature() {
179        let token = "a";
180        assert_eq!(
181            super::generate_sha1_signature(&[token, "0", "c", "b"]),
182            "a8addbc99f8b3f51d2adbceb605d650b9a8940e2",
183        );
184    }
185
186    #[test]
187    fn test_encrypt_decrypt() {
188        let token = "a";
189        let key = "cGCVnNJRgRu6wDgo7gxG2diBovGnRQq1Tqy4Rm4V4qF";
190        let agent = Agent::new(token, key);
191        let source = Source {
192            text: "abcd".to_string(),
193            receive_id: "xyz".to_string(),
194        };
195        let enc = agent.encrypt(&source);
196        let dec = agent.decrypt(enc.as_str()).unwrap();
197        assert_eq!(source, dec);
198    }
199}