Skip to main content

ncm_api_rs/
crypto.rs

1/// 加密模块 - 对应 Node.js 版本的 util/crypto.js
2///
3/// 实现三种加密方式:
4/// - weapi: 双层 AES-128-CBC + RSA
5/// - eapi: MD5 签名 + AES-128-ECB
6/// - linuxapi: AES-128-ECB
7use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyInit, KeyIvInit};
8use md5::{Digest, Md5};
9use rand::Rng;
10use rsa::traits::PublicKeyParts;
11use rsa::{BigUint, RsaPublicKey};
12use std::collections::HashMap;
13
14type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
15type Aes128EcbEnc = ecb::Encryptor<aes::Aes128>;
16type Aes128EcbDec = ecb::Decryptor<aes::Aes128>;
17
18const IV: &[u8] = b"0102030405060708";
19const PRESET_KEY: &[u8] = b"0CoJUm6Qyw8W8jud";
20const LINUXAPI_KEY: &[u8] = b"rFgB&h#%2?^eDg:Q";
21const EAPI_KEY: &[u8] = b"e82ckenh8dichen8";
22const BASE62: &[u8] = b"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
23
24// RSA 公钥 Base64 编码的 DER 数据(PKCS#8 / SubjectPublicKeyInfo 格式)
25const PUBLIC_KEY_DER_B64: &str = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDgtQn2JZ34ZC28NWYpAUd98iZ37BUrX/aKzmFbt7clFSs6sXqHauqKWqdtLkF2KexO40H1YTX8z2lSgBBOAxLsvaklV8k4cBFK9snQXE9/DDaFt6Rr7iVZMldczhC0JNgTz+SHXT6CBHuX3e9SdB1Ua44oncaTWz7OBGLbCiK45wIDAQAB";
26
27/// AES-128-CBC 加密,输出 Base64
28fn aes_cbc_encrypt_base64(plaintext: &[u8], key: &[u8], iv: &[u8]) -> String {
29    let cipher = Aes128CbcEnc::new(key.into(), iv.into());
30    let ciphertext = cipher.encrypt_padded_vec_mut::<Pkcs7>(plaintext);
31    base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &ciphertext)
32}
33
34/// AES-128-ECB 加密,输出大写 Hex
35fn aes_ecb_encrypt_hex(plaintext: &[u8], key: &[u8]) -> String {
36    let cipher = Aes128EcbEnc::new(key.into());
37    let ciphertext = cipher.encrypt_padded_vec_mut::<Pkcs7>(plaintext);
38    hex::encode_upper(&ciphertext)
39}
40
41/// AES-128-ECB 解密(输入大写 Hex)
42fn aes_ecb_decrypt_hex(ciphertext_hex: &str, key: &[u8]) -> Result<Vec<u8>, String> {
43    let ciphertext = hex::decode(ciphertext_hex).map_err(|e| e.to_string())?;
44    let cipher = Aes128EcbDec::new(key.into());
45    cipher
46        .decrypt_padded_vec_mut::<Pkcs7>(&ciphertext)
47        .map_err(|e| e.to_string())
48}
49
50/// RSA 加密(NONE / raw / textbook RSA,无 padding)
51/// 对应 Node.js: forge.publicKey.encrypt(str, 'NONE')
52fn rsa_encrypt_no_padding(plaintext: &[u8]) -> String {
53    use base64::Engine;
54    use rsa::pkcs8::DecodePublicKey;
55
56    let der_bytes = base64::engine::general_purpose::STANDARD
57        .decode(PUBLIC_KEY_DER_B64)
58        .expect("Failed to decode RSA public key base64");
59
60    let public_key =
61        RsaPublicKey::from_public_key_der(&der_bytes).expect("Failed to parse RSA public key DER");
62
63    let n = public_key.n().clone();
64    let e = public_key.e().clone();
65
66    // Textbook RSA: c = m^e mod n
67    let m = BigUint::from_bytes_be(plaintext);
68    let c = m.modpow(&e, &n);
69
70    // 输出固定长度 hex(与模数等长,256 hex chars for 1024-bit key)
71    let n_bytes = n.bits() / 8;
72    let c_bytes = c.to_bytes_be();
73
74    // 左侧填充 0
75    let mut padded = vec![0u8; n_bytes - c_bytes.len()];
76    padded.extend_from_slice(&c_bytes);
77    hex::encode(&padded)
78}
79
80/// weapi 加密
81/// 双层 AES-128-CBC + RSA 加密随机密钥
82pub fn weapi(object: &serde_json::Value) -> HashMap<String, String> {
83    let text = serde_json::to_string(object).unwrap();
84    let mut rng = rand::thread_rng();
85
86    // 生成 16 位随机密钥
87    let secret_key: String = (0..16)
88        .map(|_| BASE62[rng.gen_range(0..62)] as char)
89        .collect();
90
91    // 第一层 AES-CBC:preset_key + iv
92    let first_encrypt = aes_cbc_encrypt_base64(text.as_bytes(), PRESET_KEY, IV);
93    // 第二层 AES-CBC:secret_key + iv
94    let params = aes_cbc_encrypt_base64(first_encrypt.as_bytes(), secret_key.as_bytes(), IV);
95
96    // RSA 加密反转后的 secret_key
97    let reversed_key: String = secret_key.chars().rev().collect();
98    let enc_sec_key = rsa_encrypt_no_padding(reversed_key.as_bytes());
99
100    let mut result = HashMap::new();
101    result.insert("params".to_string(), params);
102    result.insert("encSecKey".to_string(), enc_sec_key);
103    result
104}
105
106/// linuxapi 加密
107/// 单层 AES-128-ECB
108pub fn linuxapi(object: &serde_json::Value) -> HashMap<String, String> {
109    let text = serde_json::to_string(object).unwrap();
110    let mut result = HashMap::new();
111    result.insert(
112        "eparams".to_string(),
113        aes_ecb_encrypt_hex(text.as_bytes(), LINUXAPI_KEY),
114    );
115    result
116}
117
118/// eapi 加密
119/// MD5 签名 + AES-128-ECB
120pub fn eapi(url: &str, object: &serde_json::Value) -> HashMap<String, String> {
121    let text = serde_json::to_string(object).unwrap();
122    let message = format!("nobody{}use{}md5forencrypt", url, text);
123    let digest = format!("{:x}", Md5::digest(message.as_bytes()));
124    let data = format!("{}-36cd479b6b5-{}-36cd479b6b5-{}", url, text, digest);
125
126    let mut result = HashMap::new();
127    result.insert(
128        "params".to_string(),
129        aes_ecb_encrypt_hex(data.as_bytes(), EAPI_KEY),
130    );
131    result
132}
133
134/// eapi 响应解密
135pub fn eapi_res_decrypt(encrypted_hex: &str) -> Option<serde_json::Value> {
136    let decrypted = aes_ecb_decrypt_hex(encrypted_hex, EAPI_KEY).ok()?;
137    let text = String::from_utf8(decrypted).ok()?;
138    serde_json::from_str(&text).ok()
139}
140
141/// eapi 请求解密(调试用)
142pub fn eapi_req_decrypt(encrypted_hex: &str) -> Option<(String, serde_json::Value)> {
143    let decrypted = aes_ecb_decrypt_hex(encrypted_hex, EAPI_KEY).ok()?;
144    let text = String::from_utf8(decrypted).ok()?;
145
146    // 按 "-36cd479b6b5-" 分隔符拆分
147    let parts: Vec<&str> = text.splitn(3, "-36cd479b6b5-").collect();
148    if parts.len() >= 2 {
149        let url = parts[0].to_string();
150        let data: serde_json::Value = serde_json::from_str(parts[1]).ok()?;
151        Some((url, data))
152    } else {
153        None
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn test_aes_ecb_roundtrip() {
163        let plaintext = b"hello world test";
164        let encrypted = aes_ecb_encrypt_hex(plaintext, EAPI_KEY);
165        let decrypted = aes_ecb_decrypt_hex(&encrypted, EAPI_KEY).unwrap();
166        assert_eq!(decrypted, plaintext);
167    }
168
169    #[test]
170    fn test_weapi_produces_params_and_encseckey() {
171        let obj = serde_json::json!({"id": 123});
172        let result = weapi(&obj);
173        assert!(result.contains_key("params"));
174        assert!(result.contains_key("encSecKey"));
175        assert!(!result["params"].is_empty());
176        // RSA 输出 256 hex chars (1024-bit key)
177        assert_eq!(result["encSecKey"].len(), 256);
178    }
179
180    #[test]
181    fn test_linuxapi_produces_eparams() {
182        let obj = serde_json::json!({"method": "POST", "url": "https://music.163.com/api/test"});
183        let result = linuxapi(&obj);
184        assert!(result.contains_key("eparams"));
185        assert!(!result["eparams"].is_empty());
186    }
187
188    #[test]
189    fn test_eapi_encrypt_decrypt() {
190        let url = "/api/song/detail";
191        let obj = serde_json::json!({"id": 123});
192        let encrypted = eapi(url, &obj);
193        let params = &encrypted["params"];
194
195        // 验证能解密回来
196        let (dec_url, dec_data) = eapi_req_decrypt(params).unwrap();
197        assert_eq!(dec_url, url);
198        assert_eq!(dec_data, obj);
199    }
200}